mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd29c0c9c8 | |||
| 9986d04d36 | |||
| f900d623dc | |||
| 84b507faf3 | |||
| adb27123df | |||
| a06792b923 | |||
| d90ee3ae11 | |||
| ff38afd198 | |||
| db70c06b8b | |||
| fb7656be56 | |||
| 3287e7cdec | |||
| 0761e27cfa | |||
| 4f47bb39ac | |||
| 889d071004 | |||
| 0049922ab6 | |||
| 3c995a0fff | |||
| 430719baac | |||
| 9928d711a3 | |||
| f90b43b3e1 | |||
| 64122b4cfb | |||
| 7912a942f7 | |||
| 0a7607bb62 | |||
| beeb5204b8 | |||
| ab13fb6e99 | |||
| 2a3d595a66 | |||
| 958a863d8f | |||
| 8b7884deb0 | |||
| 9a22e1c791 | |||
| f0f828491b | |||
| 7f3d107eda | |||
| ce4477eeac | |||
| 8b64f873f4 | |||
| 38bd060960 | |||
| 7c243cb6e8 | |||
| b29dee63f4 | |||
| f6542bab0a | |||
| da1b53b7e2 | |||
| 0deae95782 | |||
| 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
|
# Radarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/radarr/installation/docker)
|
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
[](#mega-sponsors)
|
[](#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.
|
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
|
## Major Features Include
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.10.2'
|
majorVersion: '5.13.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.424'
|
dotnetVersion: '6.0.427'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-12'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ class AddNewMovie extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
||||||
{translate('WhySearchesCouldBeFailing')}
|
{translate('WhySearchesCouldBeFailing')}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AddNewMovieModalContent.css';
|
import styles from './AddNewMovieModalContent.css';
|
||||||
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...minimumAvailability}
|
{...minimumAvailability}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ImportMovieHeader.css';
|
import styles from './ImportMovieHeader.css';
|
||||||
|
|
||||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
|||||||
className={styles.minimumAvailability}
|
className={styles.minimumAvailability}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
>
|
>
|
||||||
{translate('MinAvailability')}
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.detailsIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function MovieMinimumAvailabilityPopoverContent() {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Announced')}
|
||||||
|
data={translate('AnnouncedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('InCinemas')}
|
||||||
|
data={translate('InCinemasMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Released')}
|
||||||
|
data={translate('ReleasedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieMinimumAvailabilityPopoverContent;
|
||||||
@@ -31,7 +31,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import Updates from 'System/Updates/Updates';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
@@ -228,7 +228,7 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={UpdatesConnector}
|
component={Updates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
@@ -64,6 +65,7 @@ interface AppState {
|
|||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
|
movieCredits: MovieCreditAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
|
||||||
|
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
||||||
|
|
||||||
|
export default MovieCreditAppState;
|
||||||
@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTraktRating: boolean;
|
||||||
showTags: boolean;
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import IndexerFlag from 'typings/IndexerFlag';
|
|||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import General from 'typings/Settings/General';
|
import General from 'typings/Settings/General';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
@@ -29,6 +32,13 @@ export interface GeneralAppState
|
|||||||
extends AppSectionItemState<General>,
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingAppState
|
||||||
|
extends AppSectionItemState<NamingConfig>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingExamplesAppState
|
||||||
|
extends AppSectionItemState<NamingExample> {}
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -49,6 +59,12 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export interface ReleaseProfilesAppState
|
||||||
|
extends AppSectionState<ReleaseProfile>,
|
||||||
|
AppSectionSaveState {
|
||||||
|
pendingChanges: Partial<ReleaseProfile>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
export interface CustomFormatAppState
|
||||||
extends AppSectionState<CustomFormat>,
|
extends AppSectionState<CustomFormat>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -81,8 +97,11 @@ interface SettingsAppState {
|
|||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
naming: NamingAppState;
|
||||||
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
|
releaseProfiles: ReleaseProfilesAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import DiskSpace from 'typings/DiskSpace';
|
|||||||
import Health from 'typings/Health';
|
import Health from 'typings/Health';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Task from 'typings/Task';
|
import Task from 'typings/Task';
|
||||||
|
import Update from 'typings/Update';
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||||
export type HealthAppState = AppSectionState<Health>;
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type TaskAppState = AppSectionState<Task>;
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
diskSpace: DiskSpaceAppState;
|
diskSpace: DiskSpaceAppState;
|
||||||
health: HealthAppState;
|
health: HealthAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
tasks: TaskAppState;
|
tasks: TaskAppState;
|
||||||
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||||
return parseInt(input);
|
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
||||||
|
|
||||||
|
return Number(input).toFixed(numberFractionDigits);
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
const availabilityOptions = [
|
|
||||||
{
|
|
||||||
key: 'announced',
|
|
||||||
get value() {
|
|
||||||
return translate('Announced');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'inCinemas',
|
|
||||||
get value() {
|
|
||||||
return translate('InCinemas');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'released',
|
|
||||||
get value() {
|
|
||||||
return translate('Released');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function AvailabilitySelectInput(props) {
|
|
||||||
const values = [...availabilityOptions];
|
|
||||||
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
value: translate('NoChange'),
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
value: '(Mixed)',
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...props}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvailabilitySelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AvailabilitySelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AvailabilitySelectInput;
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface AvailabilitySelectInputProps {
|
||||||
|
includeNoChange: boolean;
|
||||||
|
includeNoChangeDisabled?: boolean;
|
||||||
|
includeMixed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMovieAvailabilityOption {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
format?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
|
||||||
|
{
|
||||||
|
key: 'announced',
|
||||||
|
get value() {
|
||||||
|
return translate('Announced');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'inCinemas',
|
||||||
|
get value() {
|
||||||
|
return translate('InCinemas');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'released',
|
||||||
|
get value() {
|
||||||
|
return translate('Released');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
|
||||||
|
const values = [...movieAvailabilityOptions];
|
||||||
|
|
||||||
|
const {
|
||||||
|
includeNoChange = false,
|
||||||
|
includeNoChangeDisabled = true,
|
||||||
|
includeMixed = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (includeNoChange) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'noChange',
|
||||||
|
value: translate('NoChange'),
|
||||||
|
isDisabled: includeNoChangeDisabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMixed) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'mixed',
|
||||||
|
value: `(${translate('Mixed')})`,
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EnhancedSelectInput {...props} values={values} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvailabilitySelectInput;
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './FormInputButton.css';
|
|
||||||
|
|
||||||
function FormInputButton(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
canSpin,
|
|
||||||
isLastButton,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (canSpin) {
|
|
||||||
return (
|
|
||||||
<SpinnerButton
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
isLastButton: PropTypes.bool.isRequired,
|
|
||||||
canSpin: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputButton.defaultProps = {
|
|
||||||
className: styles.button,
|
|
||||||
isLastButton: true,
|
|
||||||
canSpin: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputButton;
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import styles from './FormInputButton.css';
|
||||||
|
|
||||||
|
export interface FormInputButtonProps extends ButtonProps {
|
||||||
|
canSpin?: boolean;
|
||||||
|
isLastButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputButton({
|
||||||
|
className = styles.button,
|
||||||
|
canSpin = false,
|
||||||
|
isLastButton = true,
|
||||||
|
...otherProps
|
||||||
|
}: FormInputButtonProps) {
|
||||||
|
if (canSpin) {
|
||||||
|
return (
|
||||||
|
<SpinnerButton
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputButton;
|
||||||
@@ -272,6 +272,8 @@ FormInputGroup.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
delimiters: PropTypes.arrayOf(PropTypes.string),
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
@@ -284,8 +286,10 @@ FormInputGroup.propTypes = {
|
|||||||
helpTextWarning: PropTypes.string,
|
helpTextWarning: PropTypes.string,
|
||||||
helpLink: PropTypes.string,
|
helpLink: PropTypes.string,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
canEdit: PropTypes.bool,
|
||||||
includeNoChange: PropTypes.bool,
|
includeNoChange: PropTypes.bool,
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
|
includeAny: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
indexerFlags: PropTypes.number,
|
indexerFlags: PropTypes.number,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
|
|||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
function MovieMonitoredSelectInput(props) {
|
||||||
const values = [...monitorOptions];
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
includeNoChange,
|
includeNoChange,
|
||||||
includeMixed
|
includeMixed,
|
||||||
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const values = [...monitorOptions];
|
||||||
|
|
||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
|
|||||||
if (includeMixed) {
|
if (includeMixed) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
get value() {
|
||||||
|
return `(${translate('Mixed')})`;
|
||||||
|
},
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInput
|
<EnhancedSelectInput
|
||||||
{...props}
|
{...otherProps}
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
|
|||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import Clipboard from 'clipboard';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormInputButton from 'Components/Form/FormInputButton';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|
||||||
import styles from './ClipboardButton.css';
|
|
||||||
|
|
||||||
class ClipboardButton extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._id = getUniqueElememtId();
|
|
||||||
this._successTimeout = null;
|
|
||||||
this._testResultTimeout = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
showSuccess: false,
|
|
||||||
showError: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._clipboard = new Clipboard(`#${this._id}`, {
|
|
||||||
text: () => this.props.value,
|
|
||||||
container: document.getElementById(this._id)
|
|
||||||
});
|
|
||||||
|
|
||||||
this._clipboard.on('success', this.onSuccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const {
|
|
||||||
showSuccess,
|
|
||||||
showError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (showSuccess || showError) {
|
|
||||||
this._testResultTimeout = setTimeout(this.resetState, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._clipboard) {
|
|
||||||
this._clipboard.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._testResultTimeout) {
|
|
||||||
clearTimeout(this._testResultTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = () => {
|
|
||||||
this.setState({
|
|
||||||
showSuccess: false,
|
|
||||||
showError: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSuccess = () => {
|
|
||||||
this.setState({
|
|
||||||
showSuccess: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onError = () => {
|
|
||||||
this.setState({
|
|
||||||
showError: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
className,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
showSuccess,
|
|
||||||
showError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const showStateIcon = showSuccess || showError;
|
|
||||||
const iconName = showError ? icons.DANGER : icons.CHECK;
|
|
||||||
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormInputButton
|
|
||||||
id={this._id}
|
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<span className={showStateIcon ? styles.showStateIcon : undefined}>
|
|
||||||
{
|
|
||||||
showSuccess &&
|
|
||||||
<span className={styles.stateIconContainer}>
|
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
<span className={styles.clipboardIconContainer}>
|
|
||||||
<Icon name={icons.CLIPBOARD} />
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</FormInputButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipboardButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ClipboardButton.defaultProps = {
|
|
||||||
className: styles.button
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClipboardButton;
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import FormInputButton from 'Components/Form/FormInputButton';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { ButtonProps } from './Button';
|
||||||
|
import styles from './ClipboardButton.css';
|
||||||
|
|
||||||
|
export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClipboardState = 'success' | 'error' | null;
|
||||||
|
|
||||||
|
export default function ClipboardButton({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
className = styles.button,
|
||||||
|
...otherProps
|
||||||
|
}: ClipboardButtonProps) {
|
||||||
|
const [state, setState] = useState<ClipboardState>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setState(null);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
if ('clipboard' in navigator) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
} else {
|
||||||
|
copy(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState('success');
|
||||||
|
} catch (e) {
|
||||||
|
setState('error');
|
||||||
|
console.error(`Failed to copy to clipboard`, e);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormInputButton
|
||||||
|
className={className}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<span className={state ? styles.showStateIcon : undefined}>
|
||||||
|
{state ? (
|
||||||
|
<span className={styles.stateIconContainer}>
|
||||||
|
<Icon
|
||||||
|
name={state === 'error' ? icons.DANGER : icons.CHECK}
|
||||||
|
kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<span className={styles.clipboardIconContainer}>
|
||||||
|
<Icon name={icons.CLIPBOARD} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</FormInputButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ function getSuggestions(movies, value) {
|
|||||||
key: 'title'
|
key: 'title'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
arrayIndex: 0
|
refIndex: 0
|
||||||
});
|
});
|
||||||
if (suggestions.length > limit) {
|
if (suggestions.length > limit) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function RottenTomatoRating(props: RottenTomatoRatingProps) {
|
|||||||
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class SignalRConnector extends Component {
|
|||||||
const status = resource.status;
|
const status = resource.status;
|
||||||
|
|
||||||
// Both successful and failed commands need to be
|
// Both successful and failed commands need to be
|
||||||
// completed, otherwise they spin until they timeout.
|
// completed, otherwise they spin until they time out.
|
||||||
|
|
||||||
if (status === 'completed' || status === 'failed') {
|
if (status === 'completed' || status === 'failed') {
|
||||||
this.props.dispatchFinishCommand(resource);
|
this.props.dispatchFinishCommand(resource);
|
||||||
@@ -192,10 +192,50 @@ class SignalRConnector extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleDownloadclient = ({ action, resource }) => {
|
||||||
|
const section = 'settings.downloadClients';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleHealth = () => {
|
handleHealth = () => {
|
||||||
this.props.dispatchFetchHealth();
|
this.props.dispatchFetchHealth();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleImportlist = ({ action, resource }) => {
|
||||||
|
const section = 'settings.importLists';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleIndexer = ({ action, resource }) => {
|
||||||
|
const section = 'settings.indexers';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNotification = ({ action, resource }) => {
|
||||||
|
const section = 'settings.notifications';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleMovie = (body) => {
|
handleMovie = (body) => {
|
||||||
const action = body.action;
|
const action = body.action;
|
||||||
const section = 'movies';
|
const section = 'movies';
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function TmdbRating(props: TmdbRatingProps) {
|
|||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
align-content: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
}
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'blankpad': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import { Ratings } from 'Movie/Movie';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TraktRating.css';
|
||||||
|
|
||||||
|
interface TraktRatingProps {
|
||||||
|
ratings: Ratings;
|
||||||
|
iconSize?: number;
|
||||||
|
hideIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TraktRating(props: TraktRatingProps) {
|
||||||
|
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||||
|
|
||||||
|
const traktImage =
|
||||||
|
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
|
||||||
|
|
||||||
|
const { value = 0, votes = 0 } = ratings.trakt;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
anchor={
|
||||||
|
<span className={styles.wrapper}>
|
||||||
|
{!hideIcon && (
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
alt={translate('TraktRating')}
|
||||||
|
src={traktImage}
|
||||||
|
style={{
|
||||||
|
height: `${iconSize}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(value * 10).toFixed()}%
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
tooltip={translate('CountVotes', { votes })}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraktRating;
|
||||||
@@ -110,6 +110,15 @@ function DiscoverMovieSortMenu(props) {
|
|||||||
{translate('RottenTomatoesRating')}
|
{translate('RottenTomatoesRating')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="traktRating"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('TraktRating')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="certification"
|
name="certification"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
|
|||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -92,6 +93,7 @@ class DiscoverMoviePoster extends Component {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
ratings,
|
ratings,
|
||||||
isExisting,
|
isExisting,
|
||||||
isExcluded,
|
isExcluded,
|
||||||
@@ -223,6 +225,12 @@ class DiscoverMoviePoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showTraktRating && !!ratings.trakt ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DiscoverMoviePosterInfo
|
<DiscoverMoviePosterInfo
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
@@ -232,6 +240,7 @@ class DiscoverMoviePoster extends Component {
|
|||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTraktRating={showTraktRating}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -274,6 +283,7 @@ DiscoverMoviePoster.propTypes = {
|
|||||||
showTmdbRating: PropTypes.bool.isRequired,
|
showTmdbRating: PropTypes.bool.isRequired,
|
||||||
showImdbRating: PropTypes.bool.isRequired,
|
showImdbRating: PropTypes.bool.isRequired,
|
||||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||||
|
showTraktRating: PropTypes.bool.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
|
|||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
@@ -28,7 +29,8 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (sortKey === 'status' && status) {
|
if (sortKey === 'status' && status) {
|
||||||
@@ -141,6 +143,14 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +170,8 @@ DiscoverMoviePosterInfo.propTypes = {
|
|||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
showTmdbRating: PropTypes.bool.isRequired,
|
showTmdbRating: PropTypes.bool.isRequired,
|
||||||
showImdbRating: PropTypes.bool.isRequired,
|
showImdbRating: PropTypes.bool.isRequired,
|
||||||
showRottenTomatoesRating: PropTypes.bool.isRequired
|
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||||
|
showTraktRating: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DiscoverMoviePosterInfo;
|
export default DiscoverMoviePosterInfo;
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||||||
showTitle,
|
showTitle,
|
||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
const heights = [
|
const heights = [
|
||||||
@@ -64,6 +65,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
switch (sortKey) {
|
switch (sortKey) {
|
||||||
case 'studio':
|
case 'studio':
|
||||||
case 'inCinemas':
|
case 'inCinemas':
|
||||||
@@ -88,6 +93,11 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'traktRating':
|
||||||
|
if (!showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// No need to add a height of 0
|
// No need to add a height of 0
|
||||||
}
|
}
|
||||||
@@ -219,7 +229,8 @@ class DiscoverMoviePosters extends Component {
|
|||||||
showTitle,
|
showTitle,
|
||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||||
@@ -248,6 +259,7 @@ class DiscoverMoviePosters extends Component {
|
|||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTraktRating={showTraktRating}
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
showTmdbRating: props.showTmdbRating,
|
showTmdbRating: props.showTmdbRating,
|
||||||
showImdbRating: props.showImdbRating,
|
showImdbRating: props.showImdbRating,
|
||||||
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
||||||
|
showTraktRating: props.showTraktRating,
|
||||||
includeRecommendations: props.includeRecommendations,
|
includeRecommendations: props.includeRecommendations,
|
||||||
includeTrending: props.includeTrending,
|
includeTrending: props.includeTrending,
|
||||||
includePopular: props.includePopular
|
includePopular: props.includePopular
|
||||||
@@ -61,6 +62,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
includeRecommendations,
|
includeRecommendations,
|
||||||
includeTrending,
|
includeTrending,
|
||||||
includePopular
|
includePopular
|
||||||
@@ -88,6 +90,10 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTraktRating !== prevProps.showTraktRating) {
|
||||||
|
state.showTraktRating = showTraktRating;
|
||||||
|
}
|
||||||
|
|
||||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||||
state.includeRecommendations = includeRecommendations;
|
state.includeRecommendations = includeRecommendations;
|
||||||
}
|
}
|
||||||
@@ -140,6 +146,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
includeRecommendations,
|
includeRecommendations,
|
||||||
includeTrending,
|
includeTrending,
|
||||||
includePopular
|
includePopular
|
||||||
@@ -248,6 +255,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
onChange={this.onChangePosterOption}
|
onChange={this.onChangePosterOption}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTraktRating"
|
||||||
|
value={showTraktRating}
|
||||||
|
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||||
|
onChange={this.onChangePosterOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</Form>
|
</Form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
@@ -269,6 +288,7 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
|
|||||||
showTmdbRating: PropTypes.bool.isRequired,
|
showTmdbRating: PropTypes.bool.isRequired,
|
||||||
showImdbRating: PropTypes.bool.isRequired,
|
showImdbRating: PropTypes.bool.isRequired,
|
||||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||||
|
showTraktRating: PropTypes.bool.isRequired,
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
includeTrending: PropTypes.bool.isRequired,
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
includePopular: PropTypes.bool.isRequired,
|
includePopular: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.rottenTomatoesRating,
|
.rottenTomatoesRating,
|
||||||
|
.traktRating,
|
||||||
.runtime {
|
.runtime {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface CssExports {
|
|||||||
'status': string;
|
'status': string;
|
||||||
'studio': string;
|
'studio': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.rottenTomatoesRating,
|
.rottenTomatoesRating,
|
||||||
|
.traktRating,
|
||||||
.runtime {
|
.runtime {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface CssExports {
|
|||||||
'statusIcon': string;
|
'statusIcon': string;
|
||||||
'studio': string;
|
'studio': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -291,6 +292,17 @@ class DiscoverMovieRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'traktRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'popularity') {
|
if (name === 'popularity') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from '../MovieCreditPoster.css';
|
|
||||||
|
|
||||||
class MovieCastPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditImportListModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditImportListPress = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddImportListPress = () => {
|
|
||||||
this.props.onImportListSelect();
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListModalClose = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoad = () => {
|
|
||||||
if (this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoadError = () => {
|
|
||||||
if (!this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
personName,
|
|
||||||
character,
|
|
||||||
images,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
importList
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasPosterError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const elementStyle = {
|
|
||||||
width: `${posterWidth}px`,
|
|
||||||
height: `${posterHeight}px`,
|
|
||||||
borderRadius: '5px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentStyle = {
|
|
||||||
width: `${posterWidth}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
|
|
||||||
const importListId = importList ? importList.id : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.content}
|
|
||||||
style={contentStyle}
|
|
||||||
>
|
|
||||||
<div className={styles.posterContainer}>
|
|
||||||
<div className={styles.toggleMonitoredContainer}>
|
|
||||||
<MonitorToggleButton
|
|
||||||
className={styles.monitorToggleButton}
|
|
||||||
monitored={monitored}
|
|
||||||
size={20}
|
|
||||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Label className={styles.controls}>
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Popover
|
|
||||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
|
||||||
<Label
|
|
||||||
className={styles.externalLinkLabel}
|
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{translate('TMDb')}
|
|
||||||
</Label>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={elementStyle}
|
|
||||||
>
|
|
||||||
<MovieHeadshot
|
|
||||||
className={styles.poster}
|
|
||||||
style={elementStyle}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
lazy={false}
|
|
||||||
overflow={true}
|
|
||||||
onError={this.onPosterLoadError}
|
|
||||||
onLoad={this.onPosterLoad}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasPosterError &&
|
|
||||||
<div className={styles.overlayTitle}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{character}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditImportListModalConnector
|
|
||||||
id={importListId}
|
|
||||||
isOpen={this.state.isEditImportListModalOpen}
|
|
||||||
onModalClose={this.onEditImportListModalClose}
|
|
||||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCastPoster.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
personName: PropTypes.string.isRequired,
|
|
||||||
character: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
posterWidth: PropTypes.number.isRequired,
|
|
||||||
posterHeight: PropTypes.number.isRequired,
|
|
||||||
importList: PropTypes.object,
|
|
||||||
onImportListSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCastPoster;
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
|
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||||
|
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||||
|
import ImportList from 'typings/ImportList';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from '../MovieCreditPoster.css';
|
||||||
|
|
||||||
|
export interface MovieCastPosterProps
|
||||||
|
extends Pick<MovieCredit, 'personName' | 'images' | 'character'> {
|
||||||
|
tmdbId: number;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
importList?: ImportList;
|
||||||
|
onImportListSelect(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCastPoster(props: MovieCastPosterProps) {
|
||||||
|
const {
|
||||||
|
tmdbId,
|
||||||
|
personName,
|
||||||
|
character,
|
||||||
|
images = [],
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
importList,
|
||||||
|
onImportListSelect,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const importListId = importList?.id ?? 0;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [hasPosterError, setHasPosterError] = useState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isEditImportListModalOpen,
|
||||||
|
setEditImportListModalOpen,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handlePosterLoadError = useCallback(() => {
|
||||||
|
setHasPosterError(true);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handlePosterLoad = useCallback(() => {
|
||||||
|
setHasPosterError(false);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handleManageImportListPress = useCallback(() => {
|
||||||
|
if (importListId === 0) {
|
||||||
|
onImportListSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditImportListModalOpen();
|
||||||
|
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteImportListConfirmed = useCallback(() => {
|
||||||
|
dispatch(deleteImportList({ id: importListId }));
|
||||||
|
|
||||||
|
setEditImportListModalClosed();
|
||||||
|
setDeleteImportListModalClosed();
|
||||||
|
}, [
|
||||||
|
importListId,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitored =
|
||||||
|
importList?.enabled === true && importList?.enableAuto === true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content} style={contentStyle}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<div className={styles.toggleMonitoredContainer}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={20}
|
||||||
|
onPress={handleManageImportListPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||||
|
<Label
|
||||||
|
className={styles.externalLinkLabel}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{translate('TMDb')}
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div style={elementStyle}>
|
||||||
|
<MovieHeadshot
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={handlePosterLoadError}
|
||||||
|
onLoad={handlePosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasPosterError && (
|
||||||
|
<div className={styles.overlayTitle}>{personName}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
|
{personName}
|
||||||
|
</div>
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
|
{character}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListModalConnector
|
||||||
|
id={importListId}
|
||||||
|
isOpen={isEditImportListModalOpen}
|
||||||
|
onModalClose={setEditImportListModalClosed}
|
||||||
|
onDeleteImportListPress={setDeleteImportListModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteImportListModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteImportList')}
|
||||||
|
message={translate('DeleteImportListMessageText', {
|
||||||
|
name: importList?.name ?? personName,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={handleDeleteImportListConfirmed}
|
||||||
|
onCancel={setDeleteImportListModalClosed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCastPoster;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
|
||||||
|
import MovieCreditPosters from '../MovieCreditPosters';
|
||||||
|
import MovieCastPoster from './MovieCastPoster';
|
||||||
|
|
||||||
|
interface MovieCastPostersProps {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCastPosters({ isSmallScreen }: MovieCastPostersProps) {
|
||||||
|
const { items: castCredits } = useSelector(
|
||||||
|
createMovieCreditsSelector('cast')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MovieCreditPosters
|
||||||
|
items={castCredits}
|
||||||
|
itemComponent={MovieCastPoster}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCastPosters;
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieCreditPosters from '../MovieCreditPosters';
|
|
||||||
import MovieCastPoster from './MovieCastPoster';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCredits.items,
|
|
||||||
(credits) => {
|
|
||||||
const cast = _.reduce(credits, (acc, credit) => {
|
|
||||||
if (credit.type === 'cast') {
|
|
||||||
acc.push(credit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: cast
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCastPostersConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieCreditPosters
|
|
||||||
{...this.props}
|
|
||||||
itemComponent={MovieCastPoster}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieCastPostersConnector);
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from '../MovieCreditPoster.css';
|
|
||||||
|
|
||||||
class MovieCrewPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditImportListModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditImportListPress = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddImportListPress = () => {
|
|
||||||
this.props.onImportListSelect();
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListModalClose = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoad = () => {
|
|
||||||
if (this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoadError = () => {
|
|
||||||
if (!this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
personName,
|
|
||||||
job,
|
|
||||||
images,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
importList
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasPosterError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const elementStyle = {
|
|
||||||
width: `${posterWidth}px`,
|
|
||||||
height: `${posterHeight}px`,
|
|
||||||
borderRadius: '5px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentStyle = {
|
|
||||||
width: `${posterWidth}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
|
|
||||||
const importListId = importList ? importList.id : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.content}
|
|
||||||
style={contentStyle}
|
|
||||||
>
|
|
||||||
<div className={styles.posterContainer}>
|
|
||||||
<div className={styles.toggleMonitoredContainer}>
|
|
||||||
<MonitorToggleButton
|
|
||||||
className={styles.monitorToggleButton}
|
|
||||||
monitored={monitored}
|
|
||||||
size={20}
|
|
||||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Label className={styles.controls}>
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Popover
|
|
||||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
|
||||||
<Label
|
|
||||||
className={styles.externalLinkLabel}
|
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{translate('TMDb')}
|
|
||||||
</Label>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={elementStyle}
|
|
||||||
>
|
|
||||||
<MovieHeadshot
|
|
||||||
className={styles.poster}
|
|
||||||
style={elementStyle}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
lazy={false}
|
|
||||||
overflow={true}
|
|
||||||
onError={this.onPosterLoadError}
|
|
||||||
onLoad={this.onPosterLoad}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasPosterError &&
|
|
||||||
<div className={styles.overlayTitle}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{job}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditImportListModalConnector
|
|
||||||
id={importListId}
|
|
||||||
isOpen={this.state.isEditImportListModalOpen}
|
|
||||||
onModalClose={this.onEditImportListModalClose}
|
|
||||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCrewPoster.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
personName: PropTypes.string.isRequired,
|
|
||||||
job: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
posterWidth: PropTypes.number.isRequired,
|
|
||||||
posterHeight: PropTypes.number.isRequired,
|
|
||||||
importList: PropTypes.object,
|
|
||||||
onImportListSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCrewPoster;
|
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
|
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||||
|
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||||
|
import ImportList from 'typings/ImportList';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from '../MovieCreditPoster.css';
|
||||||
|
|
||||||
|
export interface MovieCrewPosterProps
|
||||||
|
extends Pick<MovieCredit, 'personName' | 'images' | 'job'> {
|
||||||
|
tmdbId: number;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
importList?: ImportList;
|
||||||
|
onImportListSelect(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCrewPoster(props: MovieCrewPosterProps) {
|
||||||
|
const {
|
||||||
|
tmdbId,
|
||||||
|
personName,
|
||||||
|
job,
|
||||||
|
images = [],
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
importList,
|
||||||
|
onImportListSelect,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const importListId = importList?.id ?? 0;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [hasPosterError, setHasPosterError] = useState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isEditImportListModalOpen,
|
||||||
|
setEditImportListModalOpen,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handlePosterLoadError = useCallback(() => {
|
||||||
|
setHasPosterError(true);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handlePosterLoad = useCallback(() => {
|
||||||
|
setHasPosterError(false);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handleManageImportListPress = useCallback(() => {
|
||||||
|
if (importListId === 0) {
|
||||||
|
onImportListSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditImportListModalOpen();
|
||||||
|
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteImportListConfirmed = useCallback(() => {
|
||||||
|
dispatch(deleteImportList({ id: importListId }));
|
||||||
|
|
||||||
|
setEditImportListModalClosed();
|
||||||
|
setDeleteImportListModalClosed();
|
||||||
|
}, [
|
||||||
|
importListId,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitored =
|
||||||
|
importList?.enabled === true && importList?.enableAuto === true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content} style={contentStyle}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<div className={styles.toggleMonitoredContainer}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={20}
|
||||||
|
onPress={handleManageImportListPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||||
|
<Label
|
||||||
|
className={styles.externalLinkLabel}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{translate('TMDb')}
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div style={elementStyle}>
|
||||||
|
<MovieHeadshot
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={handlePosterLoadError}
|
||||||
|
onLoad={handlePosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasPosterError && (
|
||||||
|
<div className={styles.overlayTitle}>{personName}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
|
{personName}
|
||||||
|
</div>
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
|
||||||
|
|
||||||
|
<EditImportListModalConnector
|
||||||
|
id={importListId}
|
||||||
|
isOpen={isEditImportListModalOpen}
|
||||||
|
onModalClose={setEditImportListModalClosed}
|
||||||
|
onDeleteImportListPress={setDeleteImportListModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteImportListModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteImportList')}
|
||||||
|
message={translate('DeleteImportListMessageText', {
|
||||||
|
name: importList?.name ?? personName,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={handleDeleteImportListConfirmed}
|
||||||
|
onCancel={setDeleteImportListModalClosed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCrewPoster;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
|
||||||
|
import MovieCreditPosters from '../MovieCreditPosters';
|
||||||
|
import MovieCrewPoster from './MovieCrewPoster';
|
||||||
|
|
||||||
|
interface MovieCrewPostersProps {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCrewPosters({ isSmallScreen }: MovieCrewPostersProps) {
|
||||||
|
const { items: crewCredits } = useSelector(
|
||||||
|
createMovieCreditsSelector('crew')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MovieCreditPosters
|
||||||
|
items={crewCredits}
|
||||||
|
itemComponent={MovieCrewPoster}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCrewPosters;
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieCreditPosters from '../MovieCreditPosters';
|
|
||||||
import MovieCrewPoster from './MovieCrewPoster';
|
|
||||||
|
|
||||||
function crewSort(a, b) {
|
|
||||||
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
|
|
||||||
|
|
||||||
const indexA = jobOrder.indexOf(a.job);
|
|
||||||
const indexB = jobOrder.indexOf(b.job);
|
|
||||||
|
|
||||||
if (indexA === -1 && indexB === -1) {
|
|
||||||
return 0;
|
|
||||||
} else if (indexA === -1) {
|
|
||||||
return 1;
|
|
||||||
} else if (indexB === -1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indexA < indexB) {
|
|
||||||
return -1;
|
|
||||||
} else if (indexA > indexB) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCredits.items,
|
|
||||||
(credits) => {
|
|
||||||
const crew = _.reduce(credits, (acc, credit) => {
|
|
||||||
if (credit.type === 'crew') {
|
|
||||||
acc.push(credit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sortedCrew = crew.sort(crewSort);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: _.uniqBy(sortedCrew, 'personName')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCrewPostersConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieCreditPosters
|
|
||||||
{...this.props}
|
|
||||||
itemComponent={MovieCrewPoster}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieCrewPostersConnector);
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectImportListSchema,
|
||||||
|
setImportListFieldValue,
|
||||||
|
setImportListValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createMovieCreditImportListSelector from 'Store/Selectors/createMovieCreditImportListSelector';
|
||||||
|
import { MovieCastPosterProps } from './Cast/MovieCastPoster';
|
||||||
|
import { MovieCrewPosterProps } from './Crew/MovieCrewPoster';
|
||||||
|
|
||||||
|
type MovieCreditPosterProps = {
|
||||||
|
component: React.ElementType;
|
||||||
|
} & (
|
||||||
|
| Omit<MovieCrewPosterProps, 'onImportListSelect'>
|
||||||
|
| Omit<MovieCastPosterProps, 'onImportListSelect'>
|
||||||
|
);
|
||||||
|
|
||||||
|
function MovieCreditPoster({
|
||||||
|
component: ItemComponent,
|
||||||
|
tmdbId,
|
||||||
|
personName,
|
||||||
|
...otherProps
|
||||||
|
}: MovieCreditPosterProps) {
|
||||||
|
const importList = useSelector(createMovieCreditImportListSelector(tmdbId));
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleImportListSelect = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
selectImportListSchema({
|
||||||
|
implementation: 'TMDbPersonImport',
|
||||||
|
implementationName: 'TMDb Person',
|
||||||
|
presetName: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error 'setImportListFieldValue' isn't typed yet
|
||||||
|
setImportListFieldValue({ name: 'personId', value: tmdbId.toString() })
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error 'setImportListValue' isn't typed yet
|
||||||
|
setImportListValue({ name: 'name', value: `${personName} - ${tmdbId}` })
|
||||||
|
);
|
||||||
|
}, [dispatch, tmdbId, personName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemComponent
|
||||||
|
{...otherProps}
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
personName={personName}
|
||||||
|
importList={importList}
|
||||||
|
onImportListSelect={handleImportListSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCreditPoster;
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieCreditListSelector(),
|
|
||||||
(importList) => {
|
|
||||||
return {
|
|
||||||
importList
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
selectImportListSchema,
|
|
||||||
setImportListFieldValue,
|
|
||||||
setImportListValue
|
|
||||||
};
|
|
||||||
|
|
||||||
class MovieCreditPosterConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onImportListSelect = () => {
|
|
||||||
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
|
|
||||||
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
|
|
||||||
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
component: ItemComponent,
|
|
||||||
personName
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemComponent
|
|
||||||
{...this.props}
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
personName={personName}
|
|
||||||
onImportListSelect={this.onImportListSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCreditPosterConnector.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
personName: PropTypes.string.isRequired,
|
|
||||||
component: PropTypes.elementType.isRequired,
|
|
||||||
selectImportListSchema: PropTypes.func.isRequired,
|
|
||||||
setImportListFieldValue: PropTypes.func.isRequired,
|
|
||||||
setImportListValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCreditPosterConnector);
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Navigation } from 'swiper';
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
|
||||||
import styles from './MovieCreditPosters.css';
|
|
||||||
|
|
||||||
// Import Swiper styles
|
|
||||||
import 'swiper/css';
|
|
||||||
import 'swiper/css/navigation';
|
|
||||||
|
|
||||||
// Poster container dimensions
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
|
|
||||||
function calculateRowHeight(posterHeight, isSmallScreen) {
|
|
||||||
const titleHeight = 19;
|
|
||||||
const characterHeight = 19;
|
|
||||||
|
|
||||||
const heights = [
|
|
||||||
posterHeight,
|
|
||||||
titleHeight,
|
|
||||||
characterHeight,
|
|
||||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
|
||||||
];
|
|
||||||
|
|
||||||
return heights.reduce((acc, height) => acc + height, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCreditPosters extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
columnWidth: 182,
|
|
||||||
columnCount: 1,
|
|
||||||
posterWidth: 162,
|
|
||||||
posterHeight: 238,
|
|
||||||
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
itemComponent,
|
|
||||||
isSmallScreen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
|
|
||||||
<div className={styles.sliderContainer}>
|
|
||||||
<Swiper
|
|
||||||
slidesPerView='auto'
|
|
||||||
spaceBetween={10}
|
|
||||||
slidesPerGroup={isSmallScreen ? 1 : 3}
|
|
||||||
navigation={true}
|
|
||||||
loop={false}
|
|
||||||
loopFillGroupWithBlank={true}
|
|
||||||
className="mySwiper"
|
|
||||||
modules={[Navigation]}
|
|
||||||
onInit={(swiper) => {
|
|
||||||
swiper.navigation.init();
|
|
||||||
swiper.navigation.update();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((credit) => (
|
|
||||||
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
|
|
||||||
<MovieCreditPosterConnector
|
|
||||||
key={credit.id}
|
|
||||||
component={itemComponent}
|
|
||||||
posterWidth={posterWidth}
|
|
||||||
posterHeight={posterHeight}
|
|
||||||
tmdbId={credit.personTmdbId}
|
|
||||||
personName={credit.personName}
|
|
||||||
job={credit.job}
|
|
||||||
character={credit.character}
|
|
||||||
images={credit.images}
|
|
||||||
/>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCreditPosters.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
itemComponent: PropTypes.elementType.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCreditPosters;
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Navigation } from 'swiper';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Swiper as SwiperClass } from 'swiper/types';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
import MovieCreditPoster from './MovieCreditPoster';
|
||||||
|
import styles from './MovieCreditPosters.css';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
interface MovieCreditPostersProps {
|
||||||
|
items: MovieCredit[];
|
||||||
|
itemComponent: React.ElementType;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCreditPosters(props: MovieCreditPostersProps) {
|
||||||
|
const { items, itemComponent, isSmallScreen } = props;
|
||||||
|
|
||||||
|
const posterWidth = 162;
|
||||||
|
const posterHeight = 238;
|
||||||
|
|
||||||
|
const rowHeight = useMemo(() => {
|
||||||
|
const titleHeight = 19;
|
||||||
|
const characterHeight = 19;
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
titleHeight,
|
||||||
|
characterHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}, [posterHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
const handleSwiperInit = useCallback((swiper: SwiperClass) => {
|
||||||
|
swiper.navigation.init();
|
||||||
|
swiper.navigation.update();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.sliderContainer}>
|
||||||
|
<Swiper
|
||||||
|
slidesPerView="auto"
|
||||||
|
spaceBetween={10}
|
||||||
|
slidesPerGroup={isSmallScreen ? 1 : 3}
|
||||||
|
navigation={true}
|
||||||
|
loop={false}
|
||||||
|
loopFillGroupWithBlank={true}
|
||||||
|
className="mySwiper"
|
||||||
|
modules={[Navigation]}
|
||||||
|
onInit={handleSwiperInit}
|
||||||
|
>
|
||||||
|
{items.map((credit) => (
|
||||||
|
<SwiperSlide
|
||||||
|
key={credit.id}
|
||||||
|
style={{ width: posterWidth, height: rowHeight }}
|
||||||
|
>
|
||||||
|
<MovieCreditPoster
|
||||||
|
key={credit.id}
|
||||||
|
component={itemComponent}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
tmdbId={credit.personTmdbId}
|
||||||
|
personName={credit.personName}
|
||||||
|
images={credit.images}
|
||||||
|
job={credit.job}
|
||||||
|
character={credit.character}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCreditPosters;
|
||||||
@@ -20,12 +20,14 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
|||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||||
|
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||||
@@ -37,9 +39,8 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
|
|||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
import MovieReleaseDates from './MovieReleaseDates';
|
import MovieReleaseDates from './MovieReleaseDates';
|
||||||
import MovieStatusLabel from './MovieStatusLabel';
|
import MovieStatusLabel from './MovieStatusLabel';
|
||||||
@@ -421,14 +422,15 @@ class MovieDetails extends Component {
|
|||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
!!certification &&
|
certification ?
|
||||||
<span className={styles.certification}>
|
<span className={styles.certification}>
|
||||||
{certification}
|
{certification}
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
year > 0 &&
|
year > 0 ?
|
||||||
<span className={styles.year}>
|
<span className={styles.year}>
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={
|
||||||
@@ -444,14 +446,16 @@ class MovieDetails extends Component {
|
|||||||
}
|
}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.BOTTOM}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!runtime &&
|
runtime ?
|
||||||
<span className={styles.runtime}>
|
<span className={styles.runtime}>
|
||||||
{formatRuntime(runtime, movieRuntimeFormat)}
|
{formatRuntime(runtime, movieRuntimeFormat)}
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -497,31 +501,44 @@ class MovieDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
{
|
{
|
||||||
!!ratings.tmdb &&
|
ratings.tmdb ?
|
||||||
<span className={styles.rating}>
|
<span className={styles.rating}>
|
||||||
<TmdbRating
|
<TmdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!ratings.imdb &&
|
ratings.imdb ?
|
||||||
<span className={styles.rating}>
|
<span className={styles.rating}>
|
||||||
<ImdbRating
|
<ImdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!ratings.rottenTomatoes &&
|
ratings.rottenTomatoes ?
|
||||||
<span className={styles.rating}>
|
<span className={styles.rating}>
|
||||||
<RottenTomatoRating
|
<RottenTomatoRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ratings.trakt ?
|
||||||
|
<span className={styles.rating}>
|
||||||
|
<TraktRating
|
||||||
|
ratings={ratings}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -685,13 +702,13 @@ class MovieDetails extends Component {
|
|||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Cast')}>
|
<FieldSet legend={translate('Cast')}>
|
||||||
<MovieCastPostersConnector
|
<MovieCastPosters
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Crew')}>
|
<FieldSet legend={translate('Crew')}>
|
||||||
<MovieCrewPostersConnector
|
<MovieCrewPosters
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@@ -2,23 +2,25 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieReleaseDates.css';
|
import styles from './MovieReleaseDates.css';
|
||||||
|
|
||||||
interface MovieReleaseDatesProps {
|
type MovieReleaseDatesProps = Pick<
|
||||||
inCinemas?: string;
|
Movie,
|
||||||
digitalRelease?: string;
|
'inCinemas' | 'digitalRelease' | 'physicalRelease'
|
||||||
physicalRelease?: string;
|
>;
|
||||||
}
|
|
||||||
|
|
||||||
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
function MovieReleaseDates({
|
||||||
const { inCinemas, digitalRelease, physicalRelease } = props;
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
physicalRelease,
|
||||||
createUISettingsSelector()
|
}: MovieReleaseDatesProps) {
|
||||||
);
|
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||||
|
useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
if (!inCinemas && !physicalRelease && !digitalRelease) {
|
if (!inCinemas && !physicalRelease && !digitalRelease) {
|
||||||
return (
|
return (
|
||||||
@@ -34,10 +36,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{inCinemas ? (
|
{inCinemas ? (
|
||||||
<div title={translate('InCinemas')}>
|
<div
|
||||||
|
title={`${translate('InCinemas')}: ${formatDate(
|
||||||
|
inCinemas,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<div className={styles.dateIcon}>
|
<div className={styles.dateIcon}>
|
||||||
<Icon name={icons.IN_CINEMAS} />
|
<Icon name={icons.IN_CINEMAS} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: inCinemas,
|
date: inCinemas,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
@@ -49,10 +57,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{digitalRelease ? (
|
{digitalRelease ? (
|
||||||
<div title={translate('DigitalRelease')}>
|
<div
|
||||||
|
title={`${translate('DigitalRelease')}: ${formatDate(
|
||||||
|
digitalRelease,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<div className={styles.dateIcon}>
|
<div className={styles.dateIcon}>
|
||||||
<Icon name={icons.MOVIE_FILE} />
|
<Icon name={icons.MOVIE_FILE} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: digitalRelease,
|
date: digitalRelease,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
@@ -64,10 +78,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{physicalRelease ? (
|
{physicalRelease ? (
|
||||||
<div title={translate('PhysicalRelease')}>
|
<div
|
||||||
|
title={`${translate('PhysicalRelease')}: ${formatDate(
|
||||||
|
physicalRelease,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<div className={styles.dateIcon}>
|
<div className={styles.dateIcon}>
|
||||||
<Icon name={icons.DISC} />
|
<Icon name={icons.DISC} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: physicalRelease,
|
date: physicalRelease,
|
||||||
shortDateFormat,
|
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;
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
|
||||||
|
interface MovieTitlesRowProps {
|
||||||
|
title: string;
|
||||||
|
sourceType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieTitlesRow({ title, sourceType }: MovieTitlesRowProps) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell>{title}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>{titleCase(sourceType)}</TableRowCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieTitlesRow;
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'blankpad': string;
|
||||||
'container': string;
|
'container': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
|
||||||
import styles from './MovieTitlesTable.css';
|
|
||||||
|
|
||||||
function MovieTitlesTable(props) {
|
|
||||||
const {
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<MovieTitlesTableContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesTable.propTypes = {
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieTitlesTable;
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieTitlesRow from './MovieTitlesRow';
|
||||||
|
import styles from './MovieTitlesTable.css';
|
||||||
|
|
||||||
|
const columns: Column[] = [
|
||||||
|
{
|
||||||
|
name: 'alternativeTitle',
|
||||||
|
label: () => translate('AlternativeTitle'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceType',
|
||||||
|
label: () => translate('Type'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function movieAlternativeTitlesSelector(movieId: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movies,
|
||||||
|
(movies) => {
|
||||||
|
const { isFetching, isPopulated, error, items } = movies;
|
||||||
|
|
||||||
|
const alternateTitles =
|
||||||
|
items.find((m) => m.id === movieId)?.alternateTitles ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items: alternateTitles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieTitlesProps {
|
||||||
|
movieId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieTitlesTable({ movieId }: MovieTitlesProps) {
|
||||||
|
const { isFetching, isPopulated, error, items } = useSelector(
|
||||||
|
movieAlternativeTitlesSelector(movieId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedItems = items.sort(sortByProp('title'));
|
||||||
|
|
||||||
|
if (!isFetching && !!error) {
|
||||||
|
return (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('AlternativeTitlesLoadError')}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{isFetching && <LoadingIndicator />}
|
||||||
|
|
||||||
|
{isPopulated && !items.length && !error ? (
|
||||||
|
<div className={styles.blankpad}>
|
||||||
|
{translate('NoAlternativeTitles')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!items.length && !error ? (
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{sortedItems.map((item) => (
|
||||||
|
<MovieTitlesRow
|
||||||
|
key={item.id}
|
||||||
|
title={item.title}
|
||||||
|
sourceType={item.sourceType}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieTitlesTable;
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.blankpad {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import MovieTitlesRow from './MovieTitlesRow';
|
|
||||||
import styles from './MovieTitlesTableContent.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'altTitle',
|
|
||||||
label: () => translate('AlternativeTitle'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sourceType',
|
|
||||||
label: () => translate('Type'),
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class MovieTitlesTableContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const hasItems = !!items.length;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div className={styles.blankpad}>
|
|
||||||
{translate('UnableToLoadAltTitle')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !hasItems && !error &&
|
|
||||||
<div className={styles.blankpad}>
|
|
||||||
{translate('NoAltTitle')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && hasItems && !error &&
|
|
||||||
<Table columns={columns}>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.reverse().map((item) => {
|
|
||||||
return (
|
|
||||||
<MovieTitlesRow
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesTableContent.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieTitlesTableContent;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { movieId }) => movieId,
|
|
||||||
(state) => state.movies,
|
|
||||||
(movieId, movies) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = movies;
|
|
||||||
|
|
||||||
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
alternateTitles
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieTitlesTableContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
alternateTitles,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieTitlesTableContent
|
|
||||||
{...otherProps}
|
|
||||||
items={alternateTitles}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesTableContentConnector.propTypes = {
|
|
||||||
movieId: PropTypes.number.isRequired,
|
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieTitlesTableContentConnector.defaultProps = {
|
|
||||||
alternateTitles: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
|
|
||||||
@@ -4,8 +4,6 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagInternalInput {
|
.labelIcon {
|
||||||
composes: internalInput from '~Components/Form/TagInput.css';
|
margin-left: 8px;
|
||||||
|
|
||||||
flex: 0 0 100%;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
'tagInternalInput': string;
|
'labelIcon': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditMovieModalContent.css';
|
import styles from './EditMovieModalContent.css';
|
||||||
@@ -103,7 +106,21 @@ class EditMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
|||||||
@@ -136,6 +136,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
|||||||
{translate('RottenTomatoesRating')}
|
{translate('RottenTomatoesRating')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="traktRating"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('TraktRating')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="popularity"
|
name="popularity"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
|||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
@@ -21,6 +22,7 @@ import { Statistics } from 'Movie/Movie';
|
|||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
|
import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
|
||||||
@@ -54,6 +56,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = useSelector(selectPosterOptions);
|
} = useSelector(selectPosterOptions);
|
||||||
@@ -241,7 +244,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showCinemaRelease && inCinemas ? (
|
{showCinemaRelease && inCinemas ? (
|
||||||
<div className={styles.title} title={translate('InCinemas')}>
|
<div
|
||||||
|
className={styles.title}
|
||||||
|
title={`${translate('InCinemas')}: ${formatDate(
|
||||||
|
inCinemas,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.IN_CINEMAS} />{' '}
|
<Icon name={icons.IN_CINEMAS} />{' '}
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: inCinemas,
|
date: inCinemas,
|
||||||
@@ -254,7 +263,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showDigitalRelease && digitalRelease ? (
|
{showDigitalRelease && digitalRelease ? (
|
||||||
<div className={styles.title} title={translate('DigitalRelease')}>
|
<div
|
||||||
|
className={styles.title}
|
||||||
|
title={`${translate('DigitalRelease')}: ${formatDate(
|
||||||
|
digitalRelease,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.MOVIE_FILE} />{' '}
|
<Icon name={icons.MOVIE_FILE} />{' '}
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: digitalRelease,
|
date: digitalRelease,
|
||||||
@@ -267,7 +282,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showPhysicalRelease && physicalRelease ? (
|
{showPhysicalRelease && physicalRelease ? (
|
||||||
<div className={styles.title} title={translate('PhysicalRelease')}>
|
<div
|
||||||
|
className={styles.title}
|
||||||
|
title={`${translate('PhysicalRelease')}: ${formatDate(
|
||||||
|
physicalRelease,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.DISC} />{' '}
|
<Icon name={icons.DISC} />{' '}
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: physicalRelease,
|
date: physicalRelease,
|
||||||
@@ -280,7 +301,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showReleaseDate && releaseDate ? (
|
{showReleaseDate && releaseDate ? (
|
||||||
<div className={styles.title} title={translate('ReleaseDate')}>
|
<div
|
||||||
|
className={styles.title}
|
||||||
|
title={`${translate('ReleaseDate')}: ${formatDate(
|
||||||
|
releaseDate,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.CALENDAR} />{' '}
|
<Icon name={icons.CALENDAR} />{' '}
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: releaseDate,
|
date: releaseDate,
|
||||||
@@ -310,6 +337,12 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showTraktRating && !!ratings.trakt ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{showTags && tags.length ? (
|
{showTags && tags.length ? (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
<div className={styles.tagsList}>
|
<div className={styles.tagsList}>
|
||||||
@@ -347,6 +380,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTraktRating={showTraktRating}
|
||||||
showTags={showTags}
|
showTags={showTags}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import ImdbRating from 'Components/ImdbRating';
|
|||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { Ratings } from 'Movie/Movie';
|
import { Ratings } from 'Movie/Movie';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
@@ -43,6 +45,7 @@ interface MovieIndexPosterInfoProps {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTraktRating: boolean;
|
||||||
showTags: boolean;
|
showTags: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +79,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -136,7 +140,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('InCinemas')}>
|
<div
|
||||||
|
className={styles.info}
|
||||||
|
title={`${translate('InCinemas')}: ${formatDate(
|
||||||
|
inCinemas,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
|
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -152,7 +162,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('DigitalRelease')}>
|
<div
|
||||||
|
className={styles.info}
|
||||||
|
title={`${translate('DigitalRelease')}: ${formatDate(
|
||||||
|
digitalRelease,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
|
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -172,7 +188,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('PhysicalRelease')}>
|
<div
|
||||||
|
className={styles.info}
|
||||||
|
title={`${translate('PhysicalRelease')}: ${formatDate(
|
||||||
|
physicalRelease,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.DISC} /> {physicalReleaseDate}
|
<Icon name={icons.DISC} /> {physicalReleaseDate}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -180,7 +202,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
|
|
||||||
if (sortKey === 'releaseDate' && releaseDate && !showReleaseDate) {
|
if (sortKey === 'releaseDate' && releaseDate && !showReleaseDate) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('ReleaseDate')}>
|
<div
|
||||||
|
className={styles.info}
|
||||||
|
title={`${translate('ReleaseDate')}: ${formatDate(
|
||||||
|
releaseDate,
|
||||||
|
longDateFormat
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
<Icon name={icons.CALENDAR} />{' '}
|
<Icon name={icons.CALENDAR} />{' '}
|
||||||
{getRelativeDate({
|
{getRelativeDate({
|
||||||
date: releaseDate,
|
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) {
|
if (!showTags && sortKey === 'tags' && tags.length) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
@@ -199,6 +200,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
if (showTags) {
|
if (showTags) {
|
||||||
heights.push(21);
|
heights.push(21);
|
||||||
}
|
}
|
||||||
@@ -253,6 +258,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'traktRating':
|
||||||
|
if (!showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'tags':
|
case 'tags':
|
||||||
if (!showTags) {
|
if (!showTags) {
|
||||||
heights.push(21);
|
heights.push(21);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
@@ -222,6 +223,18 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
|
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTraktRating"
|
||||||
|
value={showTraktRating}
|
||||||
|
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>{translate('ShowTags')}</FormLabel>
|
<FormLabel>{translate('ShowTags')}</FormLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
|
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.rottenTomatoesRating {
|
.rottenTomatoesRating,
|
||||||
|
.traktRating {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 80px;
|
flex: 0 0 80px;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface CssExports {
|
|||||||
'studio': string;
|
'studio': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
'year': string;
|
'year': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Column from 'Components/Table/Column';
|
|||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
@@ -387,6 +388,14 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'traktRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'popularity') {
|
if (name === 'popularity') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
|||||||
@@ -82,7 +82,8 @@
|
|||||||
|
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.rottenTomatoesRating {
|
.rottenTomatoesRating,
|
||||||
|
.traktRating {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 80px;
|
flex: 0 0 80px;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface CssExports {
|
|||||||
'studio': string;
|
'studio': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
'year': string;
|
'year': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type MovieStatus =
|
|||||||
| 'released'
|
| 'released'
|
||||||
| 'deleted';
|
| 'deleted';
|
||||||
|
|
||||||
export type CoverType = 'poster' | 'fanart';
|
export type CoverType = 'poster' | 'fanart' | 'headshot';
|
||||||
|
|
||||||
export interface Image {
|
export interface Image {
|
||||||
coverType: CoverType;
|
coverType: CoverType;
|
||||||
@@ -37,6 +37,12 @@ export interface Ratings {
|
|||||||
tmdb: RatingValues;
|
tmdb: RatingValues;
|
||||||
metacritic: RatingValues;
|
metacritic: RatingValues;
|
||||||
rottenTomatoes: RatingValues;
|
rottenTomatoes: RatingValues;
|
||||||
|
trakt: RatingValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlternativeTitle extends ModelBase {
|
||||||
|
sourceType: string;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Movie extends ModelBase {
|
interface Movie extends ModelBase {
|
||||||
@@ -52,6 +58,7 @@ interface Movie extends ModelBase {
|
|||||||
originalTitle: string;
|
originalTitle: string;
|
||||||
originalLanguage: Language;
|
originalLanguage: Language;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
|
alternateTitles: AlternativeTitle[];
|
||||||
studio: string;
|
studio: string;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
added: string;
|
added: string;
|
||||||
@@ -72,6 +79,7 @@ interface Movie extends ModelBase {
|
|||||||
images: Image[];
|
images: Image[];
|
||||||
movieFile: MovieFile;
|
movieFile: MovieFile;
|
||||||
hasFile: boolean;
|
hasFile: boolean;
|
||||||
|
lastSearchTime?: string;
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import MovieImage from './MovieImage';
|
|
||||||
|
|
||||||
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
|
||||||
|
|
||||||
function MovieHeadshot(props) {
|
|
||||||
return (
|
|
||||||
<MovieImage
|
|
||||||
{...props}
|
|
||||||
coverType="headshot"
|
|
||||||
placeholder={posterPlaceholder}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieHeadshot.propTypes = {
|
|
||||||
size: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieHeadshot.defaultProps = {
|
|
||||||
size: 250
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieHeadshot;
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MovieImage, { MovieImageProps } from './MovieImage';
|
||||||
|
|
||||||
|
const posterPlaceholder =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
||||||
|
|
||||||
|
interface MovieHeadshotProps
|
||||||
|
extends Omit<MovieImageProps, 'coverType' | 'placeholder'> {
|
||||||
|
size?: 250 | 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieHeadshot({ size = 250, ...otherProps }: MovieHeadshotProps) {
|
||||||
|
return (
|
||||||
|
<MovieImage
|
||||||
|
{...otherProps}
|
||||||
|
size={size}
|
||||||
|
coverType="headshot"
|
||||||
|
placeholder={posterPlaceholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieHeadshot;
|
||||||
@@ -43,7 +43,7 @@ function MovieImage({
|
|||||||
}: MovieImageProps) {
|
}: MovieImageProps) {
|
||||||
const [url, setUrl] = useState<string | null>(null);
|
const [url, setUrl] = useState<string | null>(null);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(true);
|
||||||
const image = useRef<Image | null>(null);
|
const image = useRef<Image | null>(null);
|
||||||
|
|
||||||
const handleLoad = useCallback(() => {
|
const handleLoad = useCallback(() => {
|
||||||
|
|||||||
+3
-3
@@ -19,7 +19,7 @@ function EditImportListExclusionModal(
|
|||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onModalClosePress = useCallback(() => {
|
const handleModalClose = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
clearPendingChanges({
|
clearPendingChanges({
|
||||||
section: 'settings.importListExclusions',
|
section: 'settings.importListExclusions',
|
||||||
@@ -29,10 +29,10 @@ function EditImportListExclusionModal(
|
|||||||
}, [dispatch, onModalClose]);
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
<EditImportListExclusionModalContent
|
<EditImportListExclusionModalContent
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
onModalClose={onModalClosePress}
|
onModalClose={handleModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
+19
-23
@@ -32,12 +32,6 @@ const newImportListExclusion = {
|
|||||||
tmdbId: 0,
|
tmdbId: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EditImportListExclusionModalContentProps {
|
|
||||||
id?: number;
|
|
||||||
onModalClose: () => void;
|
|
||||||
onDeleteImportListExclusionPress?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createImportListExclusionSelector(id?: number) {
|
function createImportListExclusionSelector(id?: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.importListExclusions,
|
(state: AppState) => state.settings.importListExclusions,
|
||||||
@@ -63,12 +57,24 @@ function createImportListExclusionSelector(id?: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditImportListExclusionModalContent(
|
interface EditImportListExclusionModalContentProps {
|
||||||
props: EditImportListExclusionModalContentProps
|
id?: number;
|
||||||
) {
|
onModalClose: () => void;
|
||||||
const { id, onModalClose, onDeleteImportListExclusionPress } = props;
|
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 dispatch = useDispatch();
|
||||||
|
const previousIsSaving = usePrevious(isSaving);
|
||||||
|
|
||||||
const dispatchSetImportListExclusionValue = (payload: {
|
const dispatchSetImportListExclusionValue = (payload: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -78,20 +84,10 @@ function EditImportListExclusionModalContent(
|
|||||||
dispatch(setImportListExclusionValue(payload));
|
dispatch(setImportListExclusionValue(payload));
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
|
|
||||||
useSelector(createImportListExclusionSelector(props.id));
|
|
||||||
const previousIsSaving = usePrevious(isSaving);
|
|
||||||
|
|
||||||
const { movieTitle, movieYear, tmdbId } = item;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
Object.keys(newImportListExclusion).forEach((name) => {
|
Object.entries(newImportListExclusion).forEach(([name, value]) => {
|
||||||
dispatchSetImportListExclusionValue({
|
dispatchSetImportListExclusionValue({ name, value });
|
||||||
name,
|
|
||||||
value:
|
|
||||||
newImportListExclusion[name as keyof typeof newImportListExclusion],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -101,7 +97,7 @@ function EditImportListExclusionModalContent(
|
|||||||
if (previousIsSaving && !isSaving && !saveError) {
|
if (previousIsSaving && !isSaving && !saveError) {
|
||||||
onModalClose();
|
onModalClose();
|
||||||
}
|
}
|
||||||
});
|
}, [previousIsSaving, isSaving, saveError, onModalClose]);
|
||||||
|
|
||||||
const onSavePress = useCallback(() => {
|
const onSavePress = useCallback(() => {
|
||||||
dispatch(saveImportListExclusion({ id }));
|
dispatch(saveImportListExclusion({ id }));
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import styles from './ImportListExclusions.css';
|
|||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'tmdbid',
|
name: 'tmdbId',
|
||||||
label: () => translate('TMDBId'),
|
label: () => translate('TMDBId'),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
composes: alert from '~Components/Alert.css';
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
|
'labelIcon': string;
|
||||||
'message': string;
|
'message': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -13,7 +15,8 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -160,12 +163,28 @@ function EditImportListModalContent(props) {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
{...minimumAvailability}
|
{...minimumAvailability}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import Card from 'Components/Card';
|
import Card from 'Components/Card';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TagList from 'Components/TagList';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -58,6 +59,8 @@ class ImportList extends Component {
|
|||||||
name,
|
name,
|
||||||
enabled,
|
enabled,
|
||||||
enableAuto,
|
enableAuto,
|
||||||
|
tags,
|
||||||
|
tagList,
|
||||||
minRefreshInterval
|
minRefreshInterval
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -72,7 +75,6 @@ class ImportList extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.enabled}>
|
<div className={styles.enabled}>
|
||||||
|
|
||||||
{
|
{
|
||||||
enabled ?
|
enabled ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
@@ -87,15 +89,21 @@ class ImportList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
enableAuto &&
|
enableAuto ?
|
||||||
<Label kind={kinds.SUCCESS}>
|
<Label kind={kinds.SUCCESS}>
|
||||||
{translate('AutomaticAdd')}
|
{translate('AutomaticAdd')}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TagList
|
||||||
|
tags={tags}
|
||||||
|
tagList={tagList}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.enabled}>
|
<div className={styles.enabled}>
|
||||||
<Label kind={kinds.INFO} title='List Refresh Interval'>
|
<Label kind={kinds.DEFAULT} title='List Refresh Interval'>
|
||||||
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
|
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,6 +134,8 @@ ImportList.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
enabled: PropTypes.bool.isRequired,
|
enabled: PropTypes.bool.isRequired,
|
||||||
enableAuto: PropTypes.bool.isRequired,
|
enableAuto: PropTypes.bool.isRequired,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
minRefreshInterval: PropTypes.string.isRequired,
|
minRefreshInterval: PropTypes.string.isRequired,
|
||||||
onConfirmDeleteImportList: PropTypes.func.isRequired
|
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class ImportLists extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
|
tagList,
|
||||||
onConfirmDeleteImportList,
|
onConfirmDeleteImportList,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -71,6 +72,7 @@ class ImportLists extends Component {
|
|||||||
<ImportList
|
<ImportList
|
||||||
key={item.id}
|
key={item.id}
|
||||||
{...item}
|
{...item}
|
||||||
|
tagList={tagList}
|
||||||
onConfirmDeleteImportList={onConfirmDeleteImportList}
|
onConfirmDeleteImportList={onConfirmDeleteImportList}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -109,6 +111,7 @@ ImportLists.propTypes = {
|
|||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onConfirmDeleteImportList: PropTypes.func.isRequired
|
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,20 @@ import { createSelector } from 'reselect';
|
|||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
|
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import ImportLists from './ImportLists';
|
import ImportLists from './ImportLists';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.importLists', sortByProp('name')),
|
createSortedSectionSelector('settings.importLists', sortByProp('name')),
|
||||||
(importLists) => importLists
|
createTagsSelector(),
|
||||||
|
(importLists, tagList) => {
|
||||||
|
return {
|
||||||
|
...importLists,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|||||||
import RootFolders from 'RootFolder/RootFolders';
|
import RootFolders from 'RootFolder/RootFolders';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import NamingConnector from './Naming/NamingConnector';
|
import Naming from './Naming/Naming';
|
||||||
import AddRootFolder from './RootFolder/AddRootFolder';
|
import AddRootFolder from './RootFolder/AddRootFolder';
|
||||||
|
|
||||||
const rescanAfterRefreshOptions = [
|
const rescanAfterRefreshOptions = [
|
||||||
@@ -106,7 +106,7 @@ class MediaManagement extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<NamingConnector />
|
<Naming />
|
||||||
|
|
||||||
{
|
{
|
||||||
isFetching ?
|
isFetching ?
|
||||||
@@ -174,24 +174,21 @@ class MediaManagement extends Component {
|
|||||||
<FieldSet
|
<FieldSet
|
||||||
legend={translate('Importing')}
|
legend={translate('Importing')}
|
||||||
>
|
>
|
||||||
{
|
<FormGroup
|
||||||
!isWindows &&
|
advancedSettings={advancedSettings}
|
||||||
<FormGroup
|
isAdvanced={true}
|
||||||
advancedSettings={advancedSettings}
|
size={sizes.MEDIUM}
|
||||||
isAdvanced={true}
|
>
|
||||||
size={sizes.MEDIUM}
|
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||||
>
|
|
||||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipFreeSpaceCheckWhenImporting"
|
name="skipFreeSpaceCheckWhenImporting"
|
||||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputButton from 'Components/Form/FormInputButton';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
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 {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isNamingModalOpen: false,
|
|
||||||
namingModalOptions: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onStandardNamingModalOpenClick = () => {
|
|
||||||
this.setState({
|
|
||||||
isNamingModalOpen: true,
|
|
||||||
namingModalOptions: {
|
|
||||||
name: 'standardMovieFormat',
|
|
||||||
additional: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMovieFolderNamingModalOpenClick = () => {
|
|
||||||
this.setState({
|
|
||||||
isNamingModalOpen: true,
|
|
||||||
namingModalOptions: {
|
|
||||||
name: 'movieFolderFormat'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onNamingModalClose = () => {
|
|
||||||
this.setState({ isNamingModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
advancedSettings,
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
settings,
|
|
||||||
hasSettings,
|
|
||||||
examples,
|
|
||||||
examplesPopulated,
|
|
||||||
onInputChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isNamingModalOpen,
|
|
||||||
namingModalOptions
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const renameMovies = hasSettings && settings.renameMovies.value;
|
|
||||||
const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value;
|
|
||||||
|
|
||||||
const standardMovieFormatHelpTexts = [];
|
|
||||||
const standardMovieFormatErrors = [];
|
|
||||||
const movieFolderFormatHelpTexts = [];
|
|
||||||
const movieFolderFormatErrors = [];
|
|
||||||
|
|
||||||
if (examplesPopulated) {
|
|
||||||
if (examples.movieExample) {
|
|
||||||
standardMovieFormatHelpTexts.push(`${translate('Movie')}: ${examples.movieExample}`);
|
|
||||||
} else {
|
|
||||||
standardMovieFormatErrors.push({ get message() {
|
|
||||||
return translate('MovieInvalidFormat');
|
|
||||||
} });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.movieFolderExample) {
|
|
||||||
movieFolderFormatHelpTexts.push(`${translate('Example')}: ${examples.movieFolderExample}`);
|
|
||||||
} else {
|
|
||||||
movieFolderFormatErrors.push({ get message() {
|
|
||||||
return translate('InvalidFormat');
|
|
||||||
} });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('MovieNaming')}>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && error &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('NamingSettingsLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
hasSettings && !isFetching && !error &&
|
|
||||||
<Form>
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
|
||||||
<FormLabel>{translate('RenameMovies')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="renameMovies"
|
|
||||||
helpText={translate('RenameMoviesHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.renameMovies}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
|
||||||
<FormLabel>{translate('ReplaceIllegalCharacters')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="replaceIllegalCharacters"
|
|
||||||
helpText={translate('ReplaceIllegalCharactersHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.replaceIllegalCharacters}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
replaceIllegalCharacters &&
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ColonReplacement')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="colonReplacementFormat"
|
|
||||||
values={colonReplacementOptions}
|
|
||||||
helpText={translate('ColonReplacementFormatHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.colonReplacementFormat}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
renameMovies &&
|
|
||||||
<FormGroup size={sizes.LARGE}>
|
|
||||||
<FormLabel>{translate('StandardMovieFormat')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
inputClassName={styles.namingInput}
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="standardMovieFormat"
|
|
||||||
buttons={<FormInputButton onPress={this.onStandardNamingModalOpenClick}>?</FormInputButton>}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.standardMovieFormat}
|
|
||||||
helpTexts={standardMovieFormatHelpTexts}
|
|
||||||
errors={[...standardMovieFormatErrors, ...settings.standardMovieFormat.errors]}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
isAdvanced={true}
|
|
||||||
>
|
|
||||||
<FormLabel>{translate('MovieFolderFormat')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
inputClassName={styles.namingInput}
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="movieFolderFormat"
|
|
||||||
buttons={<FormInputButton onPress={this.onMovieFolderNamingModalOpenClick}>?</FormInputButton>}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.movieFolderFormat}
|
|
||||||
helpTexts={[translate('MovieFolderFormatHelpText'), ...movieFolderFormatHelpTexts]}
|
|
||||||
errors={[...movieFolderFormatErrors, ...settings.movieFolderFormat.errors]}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
namingModalOptions &&
|
|
||||||
<NamingModal
|
|
||||||
isOpen={isNamingModalOpen}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
{...namingModalOptions}
|
|
||||||
value={settings[namingModalOptions.name].value}
|
|
||||||
onInputChange={onInputChange}
|
|
||||||
onModalClose={this.onNamingModalClose}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Form>
|
|
||||||
}
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Naming.propTypes = {
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
settings: PropTypes.object.isRequired,
|
|
||||||
hasSettings: PropTypes.bool.isRequired,
|
|
||||||
examples: PropTypes.object.isRequired,
|
|
||||||
examplesPopulated: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Naming;
|
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputButton from 'Components/Form/FormInputButton';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import {
|
||||||
|
fetchNamingExamples,
|
||||||
|
fetchNamingSettings,
|
||||||
|
setNamingSettingsValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import NamingModal from './NamingModal';
|
||||||
|
import styles from './Naming.css';
|
||||||
|
|
||||||
|
const SECTION = 'naming';
|
||||||
|
|
||||||
|
function createNamingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.advancedSettings,
|
||||||
|
(state: AppState) => state.settings.namingExamples,
|
||||||
|
createSettingsSectionSelector(SECTION),
|
||||||
|
(advancedSettings, namingExamples, sectionSettings) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
examples: namingExamples.item,
|
||||||
|
examplesPopulated: namingExamples.isPopulated,
|
||||||
|
...sectionSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NamingModalOptions {
|
||||||
|
name: keyof Pick<NamingConfig, 'standardMovieFormat' | 'movieFolderFormat'>;
|
||||||
|
movie?: boolean;
|
||||||
|
additional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Naming() {
|
||||||
|
const {
|
||||||
|
advancedSettings,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
settings,
|
||||||
|
hasSettings,
|
||||||
|
examples,
|
||||||
|
examplesPopulated,
|
||||||
|
} = useSelector(createNamingSelector());
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isNamingModalOpen, setNamingModalOpen, setNamingModalClosed] =
|
||||||
|
useModalOpenState(false);
|
||||||
|
const [namingModalOptions, setNamingModalOptions] =
|
||||||
|
useState<NamingModalOptions | null>(null);
|
||||||
|
const namingExampleTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchNamingSettings());
|
||||||
|
dispatch(fetchNamingExamples());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearPendingChanges({ section: SECTION }));
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: { name: string; value: string }) => {
|
||||||
|
// @ts-expect-error 'setNamingSettingsValue' isn't typed yet
|
||||||
|
dispatch(setNamingSettingsValue({ name, value }));
|
||||||
|
|
||||||
|
if (namingExampleTimeout.current) {
|
||||||
|
clearTimeout(namingExampleTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
namingExampleTimeout.current = setTimeout(() => {
|
||||||
|
dispatch(fetchNamingExamples());
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onStandardNamingModalOpenClick = useCallback(() => {
|
||||||
|
setNamingModalOpen();
|
||||||
|
|
||||||
|
setNamingModalOptions({
|
||||||
|
name: 'standardMovieFormat',
|
||||||
|
movie: true,
|
||||||
|
additional: true,
|
||||||
|
});
|
||||||
|
}, [setNamingModalOpen, setNamingModalOptions]);
|
||||||
|
|
||||||
|
const onMovieFolderNamingModalOpenClick = useCallback(() => {
|
||||||
|
setNamingModalOpen();
|
||||||
|
|
||||||
|
setNamingModalOptions({
|
||||||
|
name: 'movieFolderFormat',
|
||||||
|
});
|
||||||
|
}, [setNamingModalOpen, setNamingModalOptions]);
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
const movieFolderFormatErrors = [];
|
||||||
|
|
||||||
|
if (examplesPopulated) {
|
||||||
|
if (examples.movieExample) {
|
||||||
|
standardMovieFormatHelpTexts.push(
|
||||||
|
`${translate('Movie')}: ${examples.movieExample}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
standardMovieFormatErrors.push({
|
||||||
|
message: translate('MovieInvalidFormat'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (examples.movieFolderExample) {
|
||||||
|
movieFolderFormatHelpTexts.push(
|
||||||
|
`${translate('Example')}: ${examples.movieFolderExample}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
movieFolderFormatErrors.push({ message: translate('InvalidFormat') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('MovieNaming')}>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('NamingSettingsLoadError')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{hasSettings && !isFetching && !error ? (
|
||||||
|
<Form>
|
||||||
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
|
<FormLabel>{translate('RenameMovies')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="renameMovies"
|
||||||
|
helpText={translate('RenameMoviesHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.renameMovies}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
|
<FormLabel>{translate('ReplaceIllegalCharacters')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="replaceIllegalCharacters"
|
||||||
|
helpText={translate('ReplaceIllegalCharactersHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.replaceIllegalCharacters}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{replaceIllegalCharacters ? (
|
||||||
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
|
<FormLabel>{translate('ColonReplacement')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="colonReplacementFormat"
|
||||||
|
values={colonReplacementOptions}
|
||||||
|
helpText={translate('ColonReplacementFormatHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.colonReplacementFormat}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{renameMovies ? (
|
||||||
|
<FormGroup size={sizes.LARGE}>
|
||||||
|
<FormLabel>{translate('StandardMovieFormat')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
inputClassName={styles.namingInput}
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="standardMovieFormat"
|
||||||
|
buttons={
|
||||||
|
<FormInputButton onPress={onStandardNamingModalOpenClick}>
|
||||||
|
?
|
||||||
|
</FormInputButton>
|
||||||
|
}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.standardMovieFormat}
|
||||||
|
helpTexts={standardMovieFormatHelpTexts}
|
||||||
|
errors={[
|
||||||
|
...standardMovieFormatErrors,
|
||||||
|
...settings.standardMovieFormat.errors,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('MovieFolderFormat')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
inputClassName={styles.namingInput}
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="movieFolderFormat"
|
||||||
|
buttons={
|
||||||
|
<FormInputButton onPress={onMovieFolderNamingModalOpenClick}>
|
||||||
|
?
|
||||||
|
</FormInputButton>
|
||||||
|
}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.movieFolderFormat}
|
||||||
|
helpTexts={[
|
||||||
|
translate('MovieFolderFormatHelpText'),
|
||||||
|
...movieFolderFormatHelpTexts,
|
||||||
|
]}
|
||||||
|
errors={[
|
||||||
|
...movieFolderFormatErrors,
|
||||||
|
...settings.movieFolderFormat.errors,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{namingModalOptions ? (
|
||||||
|
<NamingModal
|
||||||
|
isOpen={isNamingModalOpen}
|
||||||
|
{...namingModalOptions}
|
||||||
|
value={settings[namingModalOptions.name].value}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onModalClose={setNamingModalClosed}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Naming;
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import { fetchNamingExamples, fetchNamingSettings, setNamingSettingsValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
|
||||||
import Naming from './Naming';
|
|
||||||
|
|
||||||
const SECTION = 'naming';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.advancedSettings,
|
|
||||||
(state) => state.settings.namingExamples,
|
|
||||||
createSettingsSectionSelector(SECTION),
|
|
||||||
(advancedSettings, examples, sectionSettings) => {
|
|
||||||
return {
|
|
||||||
advancedSettings,
|
|
||||||
examples: examples.item,
|
|
||||||
examplesPopulated: !_.isEmpty(examples.item),
|
|
||||||
...sectionSettings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchNamingSettings,
|
|
||||||
setNamingSettingsValue,
|
|
||||||
fetchNamingExamples,
|
|
||||||
clearPendingChanges
|
|
||||||
};
|
|
||||||
|
|
||||||
class NamingConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._namingExampleTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchNamingSettings();
|
|
||||||
this.props.fetchNamingExamples();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_fetchNamingExamples = () => {
|
|
||||||
this.props.fetchNamingExamples();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setNamingSettingsValue({ name, value });
|
|
||||||
|
|
||||||
if (this._namingExampleTimeout) {
|
|
||||||
clearTimeout(this._namingExampleTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._namingExampleTimeout = setTimeout(this._fetchNamingExamples, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Naming
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NamingConnector.propTypes = {
|
|
||||||
fetchNamingSettings: PropTypes.func.isRequired,
|
|
||||||
setNamingSettingsValue: PropTypes.func.isRequired,
|
|
||||||
fetchNamingExamples: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(NamingConnector);
|
|
||||||
@@ -1,506 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
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 { icons, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import NamingOption from './NamingOption';
|
|
||||||
import styles from './NamingModal.css';
|
|
||||||
|
|
||||||
const separatorOptions = [
|
|
||||||
{
|
|
||||||
key: ' ',
|
|
||||||
get value() {
|
|
||||||
return `${translate('Space')} ( )`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '.',
|
|
||||||
get value() {
|
|
||||||
return `${translate('Period')} (.)`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '_',
|
|
||||||
get value() {
|
|
||||||
return `${translate('Underscore')} (_)`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '-',
|
|
||||||
get value() {
|
|
||||||
return `${translate('Dash')} (-)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const caseOptions = [
|
|
||||||
{
|
|
||||||
key: 'title',
|
|
||||||
get value() {
|
|
||||||
return translate('DefaultCase');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'lower',
|
|
||||||
get value() {
|
|
||||||
return translate('Lowercase');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'upper',
|
|
||||||
get value() {
|
|
||||||
return translate('Uppercase');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const fileNameTokens = [
|
|
||||||
{
|
|
||||||
token: '{Movie Title} - {Quality Full}',
|
|
||||||
example: 'Movie Title (2010) - HDTV-720p Proper'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
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 TitleThe}', example: 'Movie\'s Title, The', footNote: 1 },
|
|
||||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
|
||||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
|
||||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
|
||||||
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
|
||||||
{ token: '{Movie Collection}', example: 'The Movie Collection', footNote: 1 },
|
|
||||||
{ token: '{Movie Certification}', example: 'R' },
|
|
||||||
{ token: '{Release Year}', example: '2009' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const movieIdTokens = [
|
|
||||||
{ token: '{ImdbId}', example: 'tt12345' },
|
|
||||||
{ token: '{TmdbId}', example: '123456' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const qualityTokens = [
|
|
||||||
{ token: '{Quality Full}', example: 'HDTV-720p Proper' },
|
|
||||||
{ token: '{Quality Title}', example: 'HDTV-720p' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const mediaInfoTokens = [
|
|
||||||
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
|
||||||
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: 1 },
|
|
||||||
|
|
||||||
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
|
||||||
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
|
||||||
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: 1 },
|
|
||||||
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 },
|
|
||||||
|
|
||||||
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
|
||||||
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
|
||||||
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' },
|
|
||||||
{ token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' },
|
|
||||||
{ token: '{MediaInfo 3D}', example: '3D' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const releaseGroupTokens = [
|
|
||||||
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const editionTokens = [
|
|
||||||
{ token: '{Edition Tags}', example: 'IMAX', footNote: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
const customFormatTokens = [
|
|
||||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
|
|
||||||
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const originalTokens = [
|
|
||||||
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
|
|
||||||
{ token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' }
|
|
||||||
];
|
|
||||||
|
|
||||||
class NamingModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._selectionStart = null;
|
|
||||||
this._selectionEnd = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
separator: ' ',
|
|
||||||
case: 'title'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onTokenSeparatorChange = (event) => {
|
|
||||||
this.setState({ separator: event.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTokenCaseChange = (event) => {
|
|
||||||
this.setState({ case: event.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInputSelectionChange = (selectionStart, selectionEnd) => {
|
|
||||||
this._selectionStart = selectionStart;
|
|
||||||
this._selectionEnd = selectionEnd;
|
|
||||||
};
|
|
||||||
|
|
||||||
onOptionPress = ({ isFullFilename, tokenValue }) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onInputChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const selectionStart = this._selectionStart;
|
|
||||||
const selectionEnd = this._selectionEnd;
|
|
||||||
|
|
||||||
if (isFullFilename) {
|
|
||||||
onInputChange({ name, value: tokenValue });
|
|
||||||
} else if (selectionStart == null) {
|
|
||||||
onInputChange({
|
|
||||||
name,
|
|
||||||
value: `${value}${tokenValue}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const start = value.substring(0, selectionStart);
|
|
||||||
const end = value.substring(selectionEnd);
|
|
||||||
const newValue = `${start}${tokenValue}${end}`;
|
|
||||||
|
|
||||||
onInputChange({ name, value: newValue });
|
|
||||||
this._selectionStart = newValue.length - 1;
|
|
||||||
this._selectionEnd = newValue.length - 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
isOpen,
|
|
||||||
advancedSettings,
|
|
||||||
additional,
|
|
||||||
onInputChange,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
separator: tokenSeparator,
|
|
||||||
case: tokenCase
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('FileNameTokens')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.namingSelectContainer}>
|
|
||||||
<SelectInput
|
|
||||||
className={styles.namingSelect}
|
|
||||||
name="separator"
|
|
||||||
value={tokenSeparator}
|
|
||||||
values={separatorOptions}
|
|
||||||
onChange={this.onTokenSeparatorChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
className={styles.namingSelect}
|
|
||||||
name="case"
|
|
||||||
value={tokenCase}
|
|
||||||
values={caseOptions}
|
|
||||||
onChange={this.onTokenCaseChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
!advancedSettings &&
|
|
||||||
<FieldSet legend={translate('FileNames')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
fileNameTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
isFullFilename={true}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FieldSet legend={translate('Movie')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
movieTokens.map(({ token, example, footNote }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
footNote={footNote}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
|
||||||
<InlineMarkdown data={translate('MovieFootNote')} />
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('MovieID')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
movieIdTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
{
|
|
||||||
additional &&
|
|
||||||
<div>
|
|
||||||
<FieldSet legend={translate('Quality')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
qualityTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('MediaInfo')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
mediaInfoTokens.map(({ token, example, footNote }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
footNote={footNote}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
|
||||||
<InlineMarkdown data={translate('MediaInfoFootNote')} />
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('ReleaseGroup')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
releaseGroupTokens.map(({ token, example, footNote }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
footNote={footNote}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
|
||||||
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('Edition')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
editionTokens.map(({ token, example, footNote }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
footNote={footNote}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.footNote}>
|
|
||||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
|
||||||
<InlineMarkdown data={translate('EditionFootNote')} />
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('CustomFormats')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
customFormatTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('Original')}>
|
|
||||||
<div className={styles.groups}>
|
|
||||||
{
|
|
||||||
originalTokens.map(({ token, example }) => {
|
|
||||||
return (
|
|
||||||
<NamingOption
|
|
||||||
key={token}
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
token={token}
|
|
||||||
example={example}
|
|
||||||
tokenSeparator={tokenSeparator}
|
|
||||||
tokenCase={tokenCase}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onPress={this.onOptionPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<TextInput
|
|
||||||
name={name}
|
|
||||||
value={value}
|
|
||||||
onChange={onInputChange}
|
|
||||||
onSelectionChange={this.onInputSelectionChange}
|
|
||||||
/>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NamingModal.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
additional: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
NamingModal.defaultProps = {
|
|
||||||
additional: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NamingModal;
|
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
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 { icons, sizes } from 'Helpers/Props';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import NamingOption from './NamingOption';
|
||||||
|
import TokenCase from './TokenCase';
|
||||||
|
import TokenSeparator from './TokenSeparator';
|
||||||
|
import styles from './NamingModal.css';
|
||||||
|
|
||||||
|
const separatorOptions: { key: TokenSeparator; value: string }[] = [
|
||||||
|
{
|
||||||
|
key: ' ',
|
||||||
|
get value() {
|
||||||
|
return `${translate('Space')} ( )`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '.',
|
||||||
|
get value() {
|
||||||
|
return `${translate('Period')} (.)`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '_',
|
||||||
|
get value() {
|
||||||
|
return `${translate('Underscore')} (_)`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '-',
|
||||||
|
get value() {
|
||||||
|
return `${translate('Dash')} (-)`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const caseOptions: { key: TokenCase; value: string }[] = [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
get value() {
|
||||||
|
return translate('DefaultCase');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lower',
|
||||||
|
get value() {
|
||||||
|
return translate('Lowercase');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'upper',
|
||||||
|
get value() {
|
||||||
|
return translate('Uppercase');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fileNameTokens = [
|
||||||
|
{
|
||||||
|
token:
|
||||||
|
'{Movie Title} ({Release Year}) - {Edition Tags }{[Custom Formats]}{[Quality Full]}{-Release Group}',
|
||||||
|
example:
|
||||||
|
'The Movie - Title (2010) - Ultimate Extended Edition [Surround Sound x264][Bluray-1080p Proper]-EVOLVE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token:
|
||||||
|
'{Movie CleanTitle} {Release Year} - {Edition Tags }{[Custom Formats]}{[Quality Full]}{-Release Group}',
|
||||||
|
example:
|
||||||
|
'The Movie Title 2010 - Ultimate Extended Edition [Surround Sound x264][Bluray-1080p Proper]-EVOLVE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token:
|
||||||
|
'{Movie.CleanTitle}{.Release.Year}{.Edition.Tags}{.Custom.Formats}{.Quality.Full}{-Release Group}',
|
||||||
|
example:
|
||||||
|
'The.Movie.Title.2010.Ultimate.Extended.Edition.Surround.Sound.x264.Bluray-1080p.Proper-EVOLVE',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const movieTokens = [
|
||||||
|
{ token: '{Movie Title}', example: "Movie's Title", footNote: true },
|
||||||
|
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true },
|
||||||
|
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true },
|
||||||
|
{
|
||||||
|
token: '{Movie CleanTitle:DE}',
|
||||||
|
example: 'Titel des Films',
|
||||||
|
footNote: true,
|
||||||
|
},
|
||||||
|
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true },
|
||||||
|
{
|
||||||
|
token: '{Movie CleanTitleThe}',
|
||||||
|
example: 'Movies Title, The',
|
||||||
|
footNote: true,
|
||||||
|
},
|
||||||
|
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true },
|
||||||
|
{
|
||||||
|
token: '{Movie CleanOriginalTitle}',
|
||||||
|
example: 'Τίτλος ταινίας',
|
||||||
|
footNote: true,
|
||||||
|
},
|
||||||
|
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||||
|
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
||||||
|
{
|
||||||
|
token: '{Movie Collection}',
|
||||||
|
example: 'The Movie Collection',
|
||||||
|
footNote: true,
|
||||||
|
},
|
||||||
|
{ token: '{Movie Certification}', example: 'R' },
|
||||||
|
{ token: '{Release Year}', example: '2009' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const movieIdTokens = [
|
||||||
|
{ token: '{ImdbId}', example: 'tt12345' },
|
||||||
|
{ token: '{TmdbId}', example: '123456' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const qualityTokens = [
|
||||||
|
{ token: '{Quality Full}', example: 'HDTV-720p Proper' },
|
||||||
|
{ token: '{Quality Title}', example: 'HDTV-720p' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mediaInfoTokens = [
|
||||||
|
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
||||||
|
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true },
|
||||||
|
|
||||||
|
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
||||||
|
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
||||||
|
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true },
|
||||||
|
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true },
|
||||||
|
|
||||||
|
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
||||||
|
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
||||||
|
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' },
|
||||||
|
{ token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' },
|
||||||
|
{ token: '{MediaInfo 3D}', example: '3D' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const releaseGroupTokens = [
|
||||||
|
{ token: '{Release Group}', example: 'Rls Grp', footNote: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const editionTokens = [
|
||||||
|
{ token: '{Edition Tags}', example: 'IMAX', footNote: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const customFormatTokens = [
|
||||||
|
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
|
||||||
|
{ token: '{Custom Format:FormatName}', example: 'AMZN' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const originalTokens = [
|
||||||
|
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
|
||||||
|
{ token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface NamingModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
name: keyof Pick<NamingConfig, 'standardMovieFormat' | 'movieFolderFormat'>;
|
||||||
|
value: string;
|
||||||
|
movie?: boolean;
|
||||||
|
additional?: boolean;
|
||||||
|
onInputChange: ({ name, value }: { name: string; value: string }) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NamingModal(props: NamingModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
movie = false,
|
||||||
|
additional = false,
|
||||||
|
onInputChange,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [tokenSeparator, setTokenSeparator] = useState<TokenSeparator>(' ');
|
||||||
|
const [tokenCase, setTokenCase] = useState<TokenCase>('title');
|
||||||
|
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
||||||
|
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleTokenSeparatorChange = useCallback(
|
||||||
|
({ value }: { value: TokenSeparator }) => {
|
||||||
|
setTokenSeparator(value);
|
||||||
|
},
|
||||||
|
[setTokenSeparator]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTokenCaseChange = useCallback(
|
||||||
|
({ value }: { value: TokenCase }) => {
|
||||||
|
setTokenCase(value);
|
||||||
|
},
|
||||||
|
[setTokenCase]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputSelectionChange = useCallback(
|
||||||
|
(selectionStart: number, selectionEnd: number) => {
|
||||||
|
setSelectionStart(selectionStart);
|
||||||
|
setSelectionEnd(selectionEnd);
|
||||||
|
},
|
||||||
|
[setSelectionStart, setSelectionEnd]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionPress = useCallback(
|
||||||
|
({
|
||||||
|
isFullFilename,
|
||||||
|
tokenValue,
|
||||||
|
}: {
|
||||||
|
isFullFilename: boolean;
|
||||||
|
tokenValue: string;
|
||||||
|
}) => {
|
||||||
|
if (isFullFilename) {
|
||||||
|
onInputChange({ name, value: tokenValue });
|
||||||
|
} else if (selectionStart == null || selectionEnd == null) {
|
||||||
|
onInputChange({
|
||||||
|
name,
|
||||||
|
value: `${value}${tokenValue}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const start = value.substring(0, selectionStart);
|
||||||
|
const end = value.substring(selectionEnd);
|
||||||
|
const newValue = `${start}${tokenValue}${end}`;
|
||||||
|
|
||||||
|
onInputChange({ name, value: newValue });
|
||||||
|
|
||||||
|
setSelectionStart(newValue.length - 1);
|
||||||
|
setSelectionEnd(newValue.length - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[name, value, selectionEnd, selectionStart, onInputChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{movie ? translate('FileNameTokens') : translate('FolderNameTokens')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.namingSelectContainer}>
|
||||||
|
<SelectInput
|
||||||
|
className={styles.namingSelect}
|
||||||
|
name="separator"
|
||||||
|
value={tokenSeparator}
|
||||||
|
values={separatorOptions}
|
||||||
|
onChange={handleTokenSeparatorChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
className={styles.namingSelect}
|
||||||
|
name="case"
|
||||||
|
value={tokenCase}
|
||||||
|
values={caseOptions}
|
||||||
|
onChange={handleTokenCaseChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{movie ? (
|
||||||
|
<FieldSet legend={translate('FileNames')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{fileNameTokens.map(({ token, example }) => (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
isFullFilename={true}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Movie')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{movieTokens.map(({ token, example, footNote }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('MovieFootNote')} />
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('MovieID')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{movieIdTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
{additional ? (
|
||||||
|
<div>
|
||||||
|
<FieldSet legend={translate('Quality')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{qualityTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('MediaInfo')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{mediaInfoTokens.map(({ token, example, footNote }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('MediaInfoFootNote')} />
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('ReleaseGroup')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{releaseGroupTokens.map(({ token, example, footNote }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Edition')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{editionTokens.map(({ token, example, footNote }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('EditionFootNote')} />
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('CustomFormats')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{customFormatTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Original')}>
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{originalTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
onPress={handleOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<TextInput
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onSelectionChange={handleInputSelectionChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NamingModal;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user