mirror of
https://github.com/Radarr/Radarr.git
synced 2025-04-24 06:27:08 -04:00
Convert Movie Details to TypeScript
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
This commit is contained in:
parent
c078191b3d
commit
f815b31c33
64 changed files with 1317 additions and 1701 deletions
|
@ -82,8 +82,7 @@ class AddNewMovie extends Component {
|
|||
const {
|
||||
error,
|
||||
items,
|
||||
hasExistingMovies,
|
||||
colorImpairedMode
|
||||
hasExistingMovies
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
|
@ -150,7 +149,6 @@ class AddNewMovie extends Component {
|
|||
return (
|
||||
<AddNewMovieSearchResultConnector
|
||||
key={item.tmdbId}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
|
@ -223,8 +221,7 @@ AddNewMovie.propTypes = {
|
|||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasExistingMovies: PropTypes.bool.isRequired,
|
||||
onMovieLookupChange: PropTypes.func.isRequired,
|
||||
onClearMovieLookup: PropTypes.func.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired
|
||||
onClearMovieLookup: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewMovie;
|
||||
|
|
|
@ -6,7 +6,6 @@ import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
|||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
|
@ -17,15 +16,13 @@ function createMapStateToProps() {
|
|||
(state) => state.addMovie,
|
||||
(state) => state.movies.items.length,
|
||||
(state) => state.router.location,
|
||||
createUISettingsSelector(),
|
||||
(addMovie, existingMoviesCount, location, uiSettings) => {
|
||||
(addMovie, existingMoviesCount, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
...addMovie,
|
||||
term: params.term,
|
||||
hasExistingMovies: existingMoviesCount > 0,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||
hasExistingMovies: existingMoviesCount > 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -74,12 +74,9 @@ class AddNewMovieSearchResult extends Component {
|
|||
isExistingMovie,
|
||||
isExcluded,
|
||||
isSmallScreen,
|
||||
colorImpairedMode,
|
||||
id,
|
||||
monitored,
|
||||
isAvailable,
|
||||
movieFile,
|
||||
queueItem,
|
||||
runtime,
|
||||
movieRuntimeFormat,
|
||||
certification
|
||||
|
@ -285,14 +282,12 @@ class AddNewMovieSearchResult extends Component {
|
|||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
movieId={existingMovieId}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
queueItem={queueItem}
|
||||
id={id}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
status={status}
|
||||
useLabel={true}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
@ -337,12 +332,9 @@ AddNewMovieSearchResult.propTypes = {
|
|||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isExcluded: PropTypes.bool,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
queueItem: PropTypes.object,
|
||||
colorImpairedMode: PropTypes.bool,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
certification: PropTypes.string
|
||||
|
|
|
@ -8,19 +8,16 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.movieFiles.items,
|
||||
(state, { internalId }) => internalId,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
(isExistingMovie, dimensions, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
|
||||
return {
|
||||
existingMovieId: internalId,
|
||||
isExistingMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
queueItem,
|
||||
movieFile,
|
||||
movieRuntimeFormat
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import CollectionConnector from 'Collection/CollectionConnector';
|
|||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
||||
import MovieDetailsPage from 'Movie/Details/MovieDetailsPage';
|
||||
import MovieIndex from 'Movie/Index/MovieIndex';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
|
@ -67,7 +67,7 @@ function AppRoutes() {
|
|||
|
||||
<Route path="/add/discover" component={DiscoverMovieConnector} />
|
||||
|
||||
<Route path="/movie/:titleSlug" component={MovieDetailsPageConnector} />
|
||||
<Route path="/movie/:titleSlug" component={MovieDetailsPage} />
|
||||
|
||||
{/*
|
||||
Calendar
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import ExtraFilesAppState from './ExtraFilesAppState';
|
||||
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MovieBlocklistAppState from './MovieBlocklistAppState';
|
||||
|
@ -53,6 +54,7 @@ export interface CustomFilter {
|
|||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
isSidebarVisible: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
|
@ -67,6 +69,7 @@ interface AppState {
|
|||
blocklist: BlocklistAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
extraFiles: ExtraFilesAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieBlocklist: MovieBlocklistAppState;
|
||||
|
|
6
frontend/src/App/State/ExtraFilesAppState.ts
Normal file
6
frontend/src/App/State/ExtraFilesAppState.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import { ExtraFile } from 'MovieFile/ExtraFile';
|
||||
|
||||
type ExtraFilesAppState = AppSectionState<ExtraFile>;
|
||||
|
||||
export default ExtraFilesAppState;
|
|
@ -8,17 +8,20 @@ import { kinds } from 'Helpers/Props';
|
|||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import styles from './Icon.css';
|
||||
|
||||
export type IconName = FontAwesomeIconProps['icon'];
|
||||
export type IconKind = Extract<Kind, keyof typeof styles>;
|
||||
|
||||
export interface IconProps
|
||||
extends Omit<
|
||||
FontAwesomeIconProps,
|
||||
'icon' | 'spin' | 'name' | 'title' | 'size'
|
||||
> {
|
||||
containerClassName?: ComponentProps<'span'>['className'];
|
||||
name: FontAwesomeIconProps['icon'];
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
name: IconName;
|
||||
kind?: IconKind;
|
||||
size?: number;
|
||||
isSpinning?: FontAwesomeIconProps['spin'];
|
||||
title?: string | (() => string);
|
||||
title?: string | (() => string) | null;
|
||||
}
|
||||
|
||||
export default function Icon({
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
|
||||
/** Kinds **/
|
||||
|
||||
.default {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/** Sizes **/
|
||||
|
||||
.small {
|
||||
|
|
1
frontend/src/Components/InfoLabel.css.d.ts
vendored
1
frontend/src/Components/InfoLabel.css.d.ts
vendored
|
@ -1,6 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'default': string;
|
||||
'label': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './InfoLabel.css';
|
||||
|
||||
function InfoLabel(props) {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
InfoLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
InfoLabel.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false
|
||||
};
|
||||
|
||||
export default InfoLabel;
|
41
frontend/src/Components/InfoLabel.tsx
Normal file
41
frontend/src/Components/InfoLabel.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import styles from './InfoLabel.css';
|
||||
|
||||
interface InfoLabelProps extends ComponentProps<'span'> {
|
||||
className?: string;
|
||||
name: string;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function InfoLabel({
|
||||
className = styles.label,
|
||||
name,
|
||||
kind = 'default',
|
||||
size = 'small',
|
||||
outline = false,
|
||||
children,
|
||||
...otherProps
|
||||
}: InfoLabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div>{children}</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfoLabel;
|
|
@ -1,3 +1,4 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
|
@ -7,26 +8,15 @@ const TIMEOUT = 1 / FPS * 1000;
|
|||
|
||||
class Marquee extends Component {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
hoverToStop: PropTypes.bool,
|
||||
loop: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
title: '',
|
||||
hoverToStop: true,
|
||||
loop: false
|
||||
};
|
||||
|
||||
state = {
|
||||
animatedWidth: 0,
|
||||
overflowWidth: 0,
|
||||
direction: 0
|
||||
};
|
||||
this.state = {
|
||||
animatedWidth: 0,
|
||||
overflowWidth: 0,
|
||||
direction: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.measureText();
|
||||
|
@ -138,7 +128,7 @@ class Marquee extends Component {
|
|||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`}
|
||||
className={classNames('ui-marquee', this.props.className)}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<span
|
||||
|
@ -159,7 +149,7 @@ class Marquee extends Component {
|
|||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`.trim()}
|
||||
className={classNames('ui-marquee', this.props.className)}
|
||||
style={{ overflow: 'hidden' }}
|
||||
onMouseEnter={this.onHandleMouseEnter}
|
||||
onMouseLeave={this.onHandleMouseLeave}
|
||||
|
@ -178,4 +168,20 @@ class Marquee extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
Marquee.propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
hoverToStop: PropTypes.bool,
|
||||
loop: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
Marquee.defaultProps = {
|
||||
text: '',
|
||||
title: '',
|
||||
hoverToStop: true,
|
||||
loop: false,
|
||||
className: ''
|
||||
};
|
||||
|
||||
export default Marquee;
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './PageToolbarButton.css';
|
||||
|
||||
function PageToolbarButton(props) {
|
||||
const {
|
||||
label,
|
||||
iconName,
|
||||
spinningName,
|
||||
isDisabled,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.toolbarButton,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
name={isSpinning ? (spinningName || iconName) : iconName}
|
||||
isSpinning={isSpinning}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
PageToolbarButton.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object,
|
||||
isSpinning: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
PageToolbarButton.defaultProps = {
|
||||
spinningName: icons.SPINNER,
|
||||
isDisabled: false,
|
||||
isSpinning: false
|
||||
};
|
||||
|
||||
export default PageToolbarButton;
|
49
frontend/src/Components/Page/Toolbar/PageToolbarButton.tsx
Normal file
49
frontend/src/Components/Page/Toolbar/PageToolbarButton.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Icon, { IconName } from 'Components/Icon';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './PageToolbarButton.css';
|
||||
|
||||
export interface PageToolbarButtonProps extends LinkProps {
|
||||
label: string;
|
||||
iconName: IconName;
|
||||
spinningName?: IconName;
|
||||
isSpinning?: boolean;
|
||||
isDisabled?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overflowComponent?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
function PageToolbarButton({
|
||||
label,
|
||||
iconName,
|
||||
spinningName = icons.SPINNER,
|
||||
isDisabled = false,
|
||||
isSpinning = false,
|
||||
overflowComponent,
|
||||
...otherProps
|
||||
}: PageToolbarButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.toolbarButton,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
name={isSpinning ? spinningName || iconName : iconName}
|
||||
isSpinning={isSpinning}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageToolbarButton;
|
|
@ -36,4 +36,5 @@ export type Kind =
|
|||
| 'primary'
|
||||
| 'purple'
|
||||
| 'success'
|
||||
| 'warning';
|
||||
| 'warning'
|
||||
| 'queue';
|
||||
|
|
|
@ -192,10 +192,9 @@ const importModeSelector = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
interface InteractiveImportModalContentProps {
|
||||
export interface InteractiveImportModalContentProps {
|
||||
downloadId?: string;
|
||||
movieId?: number;
|
||||
seasonNumber?: number;
|
||||
showMovie?: boolean;
|
||||
allowMovieChange?: boolean;
|
||||
showDelete?: boolean;
|
||||
|
@ -217,7 +216,6 @@ function InteractiveImportModalContent(
|
|||
const {
|
||||
downloadId,
|
||||
movieId,
|
||||
seasonNumber,
|
||||
allowMovieChange = true,
|
||||
showMovie = true,
|
||||
showFilterExistingFiles = false,
|
||||
|
@ -343,7 +341,6 @@ function InteractiveImportModalContent(
|
|||
fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
movieId,
|
||||
seasonNumber,
|
||||
folder,
|
||||
filterExistingFiles,
|
||||
})
|
||||
|
|
|
@ -4,9 +4,12 @@ import usePrevious from 'Helpers/Hooks/usePrevious';
|
|||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveImportSelectFolderModalContent from './Folder/InteractiveImportSelectFolderModalContent';
|
||||
import InteractiveImportModalContent from './Interactive/InteractiveImportModalContent';
|
||||
import InteractiveImportModalContent, {
|
||||
InteractiveImportModalContentProps,
|
||||
} from './Interactive/InteractiveImportModalContent';
|
||||
|
||||
interface InteractiveImportModalProps {
|
||||
interface InteractiveImportModalProps
|
||||
extends Omit<InteractiveImportModalContentProps, 'modalTitle'> {
|
||||
isOpen: boolean;
|
||||
folder?: string;
|
||||
downloadId?: string;
|
||||
|
|
|
@ -160,9 +160,8 @@
|
|||
}
|
||||
|
||||
.overview {
|
||||
flex: 1 0 auto;
|
||||
flex: 1 0 0;
|
||||
margin-top: 8px;
|
||||
padding-left: 7px;
|
||||
min-height: 0;
|
||||
font-size: $intermediateFontSize;
|
||||
}
|
||||
|
|
|
@ -1,835 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import InfoLabel from 'Components/InfoLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Marquee from 'Components/Marquee';
|
||||
import Measure from 'Components/Measure';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MovieCollectionLabel from 'Movie/MovieCollectionLabel';
|
||||
import MovieGenres from 'Movie/MovieGenres';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
import MovieStatusLabel from './MovieStatusLabel';
|
||||
import MovieTagsConnector from './MovieTagsConnector';
|
||||
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
||||
import styles from './MovieDetails.css';
|
||||
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function getFanartUrl(images) {
|
||||
const image = images.find((img) => img.coverType === 'fanart');
|
||||
return image?.url ?? image?.remoteUrl;
|
||||
}
|
||||
|
||||
class MovieDetails extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isOrganizeModalOpen: false,
|
||||
isEditMovieModalOpen: false,
|
||||
isDeleteMovieModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isInteractiveSearchModalOpen: false,
|
||||
isMovieHistoryModalOpen: false,
|
||||
overviewHeight: 0,
|
||||
titleWidth: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('touchstart', this.onTouchStart);
|
||||
window.addEventListener('touchend', this.onTouchEnd);
|
||||
window.addEventListener('touchcancel', this.onTouchCancel);
|
||||
window.addEventListener('touchmove', this.onTouchMove);
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('touchstart', this.onTouchStart);
|
||||
window.removeEventListener('touchend', this.onTouchEnd);
|
||||
window.removeEventListener('touchcancel', this.onTouchCancel);
|
||||
window.removeEventListener('touchmove', this.onTouchMove);
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOrganizePress = () => {
|
||||
this.setState({ isOrganizeModalOpen: true });
|
||||
};
|
||||
|
||||
onOrganizeModalClose = () => {
|
||||
this.setState({ isOrganizeModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
};
|
||||
|
||||
onEditMoviePress = () => {
|
||||
this.setState({ isEditMovieModalOpen: true });
|
||||
};
|
||||
|
||||
onEditMovieModalClose = () => {
|
||||
this.setState({ isEditMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveSearchPress = () => {
|
||||
this.setState({ isInteractiveSearchModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveSearchModalClose = () => {
|
||||
this.setState({ isInteractiveSearchModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteMoviePress = () => {
|
||||
this.setState({
|
||||
isEditMovieModalOpen: false,
|
||||
isDeleteMovieModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteMovieModalClose = () => {
|
||||
this.setState({ isDeleteMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onMovieHistoryPress = () => {
|
||||
this.setState({ isMovieHistoryModalOpen: true });
|
||||
};
|
||||
|
||||
onMovieHistoryModalClose = () => {
|
||||
this.setState({ isMovieHistoryModalOpen: false });
|
||||
};
|
||||
|
||||
onMeasure = ({ height }) => {
|
||||
this.setState({ overviewHeight: height });
|
||||
};
|
||||
|
||||
onTitleMeasure = ({ width }) => {
|
||||
this.setState({ titleWidth: width });
|
||||
};
|
||||
|
||||
onKeyUp = (event) => {
|
||||
if (event.composedPath && event.composedPath().length === 4) {
|
||||
if (event.keyCode === keyCodes.LEFT_ARROW) {
|
||||
this.props.onGoToMovie(this.props.previousMovie.titleSlug);
|
||||
}
|
||||
if (event.keyCode === keyCodes.RIGHT_ARROW) {
|
||||
this.props.onGoToMovie(this.props.nextMovie.titleSlug);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onTouchStart = (event) => {
|
||||
const touches = event.touches;
|
||||
const touchStart = touches[0].pageX;
|
||||
const touchY = touches[0].pageY;
|
||||
|
||||
// Only change when swipe is on header, we need horizontal scroll on tables
|
||||
if (touchY > 470) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (touches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
touchStart < 50 ||
|
||||
this.props.isSidebarVisible ||
|
||||
this.state.isOrganizeModalOpen ||
|
||||
this.state.isEditMovieModalOpen ||
|
||||
this.state.isDeleteMovieModalOpen ||
|
||||
this.state.isInteractiveImportModalOpen ||
|
||||
this.state.isInteractiveSearchModalOpen ||
|
||||
this.state.isMovieHistoryModalOpen
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchStart = touchStart;
|
||||
};
|
||||
|
||||
onTouchEnd = (event) => {
|
||||
const touches = event.changedTouches;
|
||||
const currentTouch = touches[0].pageX;
|
||||
|
||||
if (!this._touchStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) {
|
||||
this.props.onGoToMovie(this.props.previousMovie.titleSlug);
|
||||
} else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) {
|
||||
this.props.onGoToMovie(this.props.nextMovie.titleSlug);
|
||||
}
|
||||
|
||||
this._touchStart = null;
|
||||
};
|
||||
|
||||
onTouchCancel = (event) => {
|
||||
this._touchStart = null;
|
||||
};
|
||||
|
||||
onTouchMove = (event) => {
|
||||
if (!this._touchStart) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
title,
|
||||
originalTitle,
|
||||
year,
|
||||
inCinemas,
|
||||
physicalRelease,
|
||||
digitalRelease,
|
||||
runtime,
|
||||
certification,
|
||||
ratings,
|
||||
path,
|
||||
statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
studio,
|
||||
originalLanguage,
|
||||
genres,
|
||||
collection,
|
||||
overview,
|
||||
status,
|
||||
youTubeTrailerId,
|
||||
isAvailable,
|
||||
images,
|
||||
tags,
|
||||
isSaving,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
isFetching,
|
||||
isSmallScreen,
|
||||
movieFilesError,
|
||||
movieCreditsError,
|
||||
extraFilesError,
|
||||
hasMovieFiles,
|
||||
previousMovie,
|
||||
nextMovie,
|
||||
onMonitorTogglePress,
|
||||
onRefreshPress,
|
||||
onSearchPress,
|
||||
queueItem,
|
||||
movieRuntimeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isEditMovieModalOpen,
|
||||
isDeleteMovieModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
isMovieHistoryModalOpen,
|
||||
overviewHeight,
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getMovieStatusDetails(status);
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||
|
||||
const titleWithYear = `${title}${year > 0 ? ` (${year})` : ''}`;
|
||||
|
||||
return (
|
||||
<PageContent title={titleWithYear}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshAndScan')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
title={translate('RefreshInformationAndScanDisk')}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SearchMovie')}
|
||||
iconName={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
title={undefined}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('InteractiveSearch')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
isSpinning={isSearching}
|
||||
title={undefined}
|
||||
onPress={this.onInteractiveSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('PreviewRename')}
|
||||
iconName={icons.ORGANIZE}
|
||||
isDisabled={!hasMovieFiles}
|
||||
onPress={this.onOrganizePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageFiles')}
|
||||
iconName={icons.MOVIE_FILE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('History')}
|
||||
iconName={icons.HISTORY}
|
||||
onPress={this.onMovieHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Edit')}
|
||||
iconName={icons.EDIT}
|
||||
onPress={this.onEditMoviePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Delete')}
|
||||
iconName={icons.DELETE}
|
||||
onPress={this.onDeleteMoviePress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
style={
|
||||
fanartUrl ?
|
||||
{ backgroundImage: `url(${fanartUrl})` } :
|
||||
null
|
||||
}
|
||||
>
|
||||
<div className={styles.backdropOverlay} />
|
||||
</div>
|
||||
|
||||
<div className={styles.headerContent}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={500}
|
||||
lazy={false}
|
||||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<Measure onMeasure={this.onTitleMeasure}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={40}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.title} style={{ width: marqueeWidth }}>
|
||||
<Marquee text={title} title={originalTitle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.movieNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.movieNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={translate('GoToInterp', [previousMovie.title])}
|
||||
to={`/movie/${previousMovie.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.movieNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={translate('GoToInterp', [nextMovie.title])}
|
||||
to={`/movie/${nextMovie.titleSlug}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Measure>
|
||||
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
certification ?
|
||||
<span className={styles.certification} title={translate('Certification')}>
|
||||
{certification}
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
<span className={styles.year}>
|
||||
<Popover
|
||||
anchor={
|
||||
year > 0 ? (
|
||||
year
|
||||
) : (
|
||||
<Icon
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
size={20}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={translate('ReleaseDates')}
|
||||
body={
|
||||
<MovieReleaseDates
|
||||
tmdbId={tmdbId}
|
||||
inCinemas={inCinemas}
|
||||
digitalRelease={digitalRelease}
|
||||
physicalRelease={physicalRelease}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{
|
||||
runtime ?
|
||||
<span className={styles.runtime} title={translate('Runtime')}>
|
||||
{formatRuntime(runtime, movieRuntimeFormat)}
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
<span className={styles.links}>
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
imdbId={imdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
!!tags.length &&
|
||||
<span>
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.TAGS}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
tooltip={
|
||||
<MovieTagsConnector movieId={id} />
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
{
|
||||
ratings.tmdb ?
|
||||
<span className={styles.rating}>
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
ratings.imdb ?
|
||||
<span className={styles.rating}>
|
||||
<ImdbRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
ratings.rottenTomatoes ?
|
||||
<span className={styles.rating}>
|
||||
<RottenTomatoRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
ratings.trakt ?
|
||||
<span className={styles.rating}>
|
||||
<TraktRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.detailsLabels}>
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Path')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Status')}
|
||||
title={statusDetails.message}
|
||||
kind={kinds.DELETE}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.statusName}>
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFiles}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
queueItem={queueItem}
|
||||
/>
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('QualityProfile')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Size')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
{
|
||||
collection ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Collection')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div className={styles.collection}>
|
||||
<MovieCollectionLabel
|
||||
tmdbId={collection.tmdbId}
|
||||
/>
|
||||
</div>
|
||||
</InfoLabel> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
originalLanguage?.name && !isSmallScreen ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('OriginalLanguage')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.originalLanguage}>
|
||||
{originalLanguage.name}
|
||||
</span>
|
||||
</InfoLabel> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
studio && !isSmallScreen ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Studio')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.studio}>
|
||||
{studio}
|
||||
</span>
|
||||
</InfoLabel> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
genres.length && !isSmallScreen ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Genres')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<MovieGenres className={styles.genres} genres={genres} />
|
||||
</InfoLabel> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</Measure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isFetching && movieFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingMovieFilesFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && movieCreditsError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingMovieCreditsFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && extraFilesError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingMovieExtraFilesFailed')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
<FieldSet legend={translate('Files')}>
|
||||
<MovieFileEditorTable
|
||||
movieId={id}
|
||||
/>
|
||||
|
||||
<ExtraFileTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Cast')}>
|
||||
<MovieCastPosters
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Crew')}>
|
||||
<MovieCrewPosters
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Titles')}>
|
||||
<MovieTitlesTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModal
|
||||
isOpen={isOrganizeModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onEditMovieModalClose}
|
||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
||||
/>
|
||||
|
||||
<MovieHistoryModal
|
||||
isOpen={isMovieHistoryModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onMovieHistoryModalClose}
|
||||
/>
|
||||
|
||||
<DeleteMovieModal
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onDeleteMovieModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
movieId={id}
|
||||
title={title}
|
||||
folder={path}
|
||||
initialSortKey="relativePath"
|
||||
initialSortDirection={sortDirections.ASCENDING}
|
||||
showMovie={false}
|
||||
allowMovieChange={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={translate('ManageFiles')}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<MovieInteractiveSearchModal
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
onModalClose={this.onInteractiveSearchModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieDetails.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
imdbId: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
originalTitle: PropTypes.string,
|
||||
year: PropTypes.number.isRequired,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
certification: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
studio: PropTypes.string,
|
||||
originalLanguage: PropTypes.object,
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
collection: PropTypes.object,
|
||||
youTubeTrailerId: PropTypes.string,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
physicalRelease: PropTypes.string,
|
||||
digitalRelease: PropTypes.string,
|
||||
overview: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isSidebarVisible: PropTypes.bool.isRequired,
|
||||
movieFilesError: PropTypes.object,
|
||||
movieCreditsError: PropTypes.object,
|
||||
extraFilesError: PropTypes.object,
|
||||
hasMovieFiles: PropTypes.bool.isRequired,
|
||||
previousMovie: PropTypes.object.isRequired,
|
||||
nextMovie: PropTypes.object.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
onGoToMovie: PropTypes.func.isRequired,
|
||||
queueItem: PropTypes.object,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
MovieDetails.defaultProps = {
|
||||
genres: [],
|
||||
statistics: {},
|
||||
tags: [],
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
export default MovieDetails;
|
956
frontend/src/Movie/Details/MovieDetails.tsx
Normal file
956
frontend/src/Movie/Details/MovieDetails.tsx
Normal file
|
@ -0,0 +1,956 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import InfoLabel from 'Components/InfoLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Marquee from 'Components/Marquee';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
icons,
|
||||
kinds,
|
||||
sizes,
|
||||
sortDirections,
|
||||
tooltipPositions,
|
||||
} from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import { Image, Statistics } from 'Movie/Movie';
|
||||
import MovieCollectionLabel from 'Movie/MovieCollectionLabel';
|
||||
import MovieGenres from 'Movie/MovieGenres';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
clearExtraFiles,
|
||||
fetchExtraFiles,
|
||||
} from 'Store/Actions/extraFileActions';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import {
|
||||
clearMovieCredits,
|
||||
fetchMovieCredits,
|
||||
} from 'Store/Actions/movieCreditsActions';
|
||||
import {
|
||||
clearMovieFiles,
|
||||
fetchMovieFiles,
|
||||
} from 'Store/Actions/movieFileActions';
|
||||
import {
|
||||
clearQueueDetails,
|
||||
fetchQueueDetails,
|
||||
} from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportListSchema } from 'Store/Actions/Settings/importLists';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
import MovieStatusLabel from './MovieStatusLabel';
|
||||
import MovieTags from './MovieTags';
|
||||
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
||||
import styles from './MovieDetails.css';
|
||||
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function getFanartUrl(images: Image[]) {
|
||||
const image = images.find((image) => image.coverType === 'fanart');
|
||||
return image?.url ?? image?.remoteUrl;
|
||||
}
|
||||
|
||||
function createMovieFilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movieFiles,
|
||||
({ items, isFetching, isPopulated, error }) => {
|
||||
const hasMovieFiles = !!items.length;
|
||||
|
||||
return {
|
||||
isMovieFilesFetching: isFetching,
|
||||
isMovieFilesPopulated: isPopulated,
|
||||
movieFilesError: error,
|
||||
hasMovieFiles,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createExtraFilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.extraFiles,
|
||||
({ isFetching, isPopulated, error }) => {
|
||||
return {
|
||||
isExtraFilesFetching: isFetching,
|
||||
isExtraFilesPopulated: isPopulated,
|
||||
extraFilesError: error,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMovieCreditsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movieCredits,
|
||||
({ isFetching, isPopulated, error }) => {
|
||||
return {
|
||||
isMovieCreditsFetching: isFetching,
|
||||
isMovieCreditsPopulated: isPopulated,
|
||||
movieCreditsError: error,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMovieSelector(movieId: number) {
|
||||
return createSelector(createAllMoviesSelector(), (allMovies) => {
|
||||
const sortedMovies = [...allMovies].sort(sortByProp('sortTitle'));
|
||||
const movieIndex = sortedMovies.findIndex((movie) => movie.id === movieId);
|
||||
|
||||
if (movieIndex === -1) {
|
||||
return {
|
||||
movie: undefined,
|
||||
nextMovie: undefined,
|
||||
previousMovie: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const movie = sortedMovies[movieIndex];
|
||||
const nextMovie = sortedMovies[movieIndex + 1] ?? sortedMovies[0];
|
||||
const previousMovie =
|
||||
sortedMovies[movieIndex - 1] ?? sortedMovies[sortedMovies.length - 1];
|
||||
|
||||
return {
|
||||
movie,
|
||||
nextMovie: {
|
||||
title: nextMovie.title,
|
||||
titleSlug: nextMovie.titleSlug,
|
||||
},
|
||||
previousMovie: {
|
||||
title: previousMovie.title,
|
||||
titleSlug: previousMovie.titleSlug,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface MovieDetailsProps {
|
||||
movieId: number;
|
||||
}
|
||||
|
||||
function MovieDetails({ movieId }: MovieDetailsProps) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { movie, nextMovie, previousMovie } = useSelector(
|
||||
createMovieSelector(movieId)
|
||||
);
|
||||
const { isMovieFilesFetching, movieFilesError, hasMovieFiles } = useSelector(
|
||||
createMovieFilesSelector()
|
||||
);
|
||||
const { isExtraFilesFetching, extraFilesError } = useSelector(
|
||||
createExtraFilesSelector()
|
||||
);
|
||||
const { isMovieCreditsFetching, movieCreditsError } = useSelector(
|
||||
createMovieCreditsSelector()
|
||||
);
|
||||
const { movieRuntimeFormat } = useSelector(createUISettingsSelector());
|
||||
const isSidebarVisible = useSelector(
|
||||
(state: AppState) => state.app.isSidebarVisible
|
||||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
|
||||
const commands = useSelector(createCommandsSelector());
|
||||
const isSaving = useSelector((state: AppState) => state.movies.isSaving);
|
||||
|
||||
const { isRefreshing, isRenaming, isSearching } = useMemo(() => {
|
||||
const movieRefreshingCommand = findCommand(commands, {
|
||||
name: commandNames.REFRESH_MOVIE,
|
||||
});
|
||||
|
||||
const isMovieRefreshingCommandExecuting = isCommandExecuting(
|
||||
movieRefreshingCommand
|
||||
);
|
||||
|
||||
const allMoviesRefreshing =
|
||||
isMovieRefreshingCommandExecuting &&
|
||||
!movieRefreshingCommand?.body.movieIds?.length;
|
||||
|
||||
const isMovieRefreshing =
|
||||
isMovieRefreshingCommandExecuting &&
|
||||
movieRefreshingCommand?.body.movieIds?.includes(movieId);
|
||||
|
||||
const isSearchingExecuting = isCommandExecuting(
|
||||
findCommand(commands, {
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: [movieId],
|
||||
})
|
||||
);
|
||||
|
||||
const isRenamingFiles = isCommandExecuting(
|
||||
findCommand(commands, {
|
||||
name: commandNames.RENAME_FILES,
|
||||
movieId,
|
||||
})
|
||||
);
|
||||
|
||||
const isRenamingMovieCommand = findCommand(commands, {
|
||||
name: commandNames.RENAME_MOVIE,
|
||||
});
|
||||
|
||||
const isRenamingMovie =
|
||||
isCommandExecuting(isRenamingMovieCommand) &&
|
||||
isRenamingMovieCommand?.body?.movieIds?.includes(movieId);
|
||||
|
||||
return {
|
||||
isRefreshing: isMovieRefreshing || allMoviesRefreshing,
|
||||
isRenaming: isRenamingFiles || isRenamingMovie,
|
||||
isSearching: isSearchingExecuting,
|
||||
};
|
||||
}, [movieId, commands]);
|
||||
|
||||
const touchStart = useRef<number | null>(null);
|
||||
const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false);
|
||||
const [isManageMoviesModalOpen, setIsManageMoviesModalOpen] = useState(false);
|
||||
const [isInteractiveSearchModalOpen, setIsInteractiveSearchModalOpen] =
|
||||
useState(false);
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||
const [isMovieHistoryModalOpen, setIsMovieHistoryModalOpen] = useState(false);
|
||||
const [titleRef, { width: titleWidth }] = useMeasure();
|
||||
const [overviewRef, { height: overviewHeight }] = useMeasure();
|
||||
const wasRefreshing = usePrevious(isRefreshing);
|
||||
const wasRenaming = usePrevious(isRenaming);
|
||||
|
||||
const handleOrganizePress = useCallback(() => {
|
||||
setIsOrganizeModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleOrganizeModalClose = useCallback(() => {
|
||||
setIsOrganizeModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleManageMoviesPress = useCallback(() => {
|
||||
setIsManageMoviesModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleManageMoviesModalClose = useCallback(() => {
|
||||
setIsManageMoviesModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleInteractiveSearchPress = useCallback(() => {
|
||||
setIsInteractiveSearchModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleInteractiveSearchModalClose = useCallback(() => {
|
||||
setIsInteractiveSearchModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleEditMoviePress = useCallback(() => {
|
||||
setIsEditMovieModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditMovieModalClose = useCallback(() => {
|
||||
setIsEditMovieModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteMoviePress = useCallback(() => {
|
||||
setIsEditMovieModalOpen(false);
|
||||
setIsDeleteMovieModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteMovieModalClose = useCallback(() => {
|
||||
setIsDeleteMovieModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleMovieHistoryPress = useCallback(() => {
|
||||
setIsMovieHistoryModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleMovieHistoryModalClose = useCallback(() => {
|
||||
setIsMovieHistoryModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleMonitorTogglePress = useCallback(
|
||||
(value: boolean) => {
|
||||
dispatch(
|
||||
toggleMovieMonitored({
|
||||
movieId,
|
||||
monitored: value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[movieId, dispatch]
|
||||
);
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.REFRESH_MOVIE,
|
||||
movieIds: [movieId],
|
||||
})
|
||||
);
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
const handleSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: [movieId],
|
||||
})
|
||||
);
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
const handleTouchStart = useCallback(
|
||||
(event: TouchEvent) => {
|
||||
const touches = event.touches;
|
||||
const currentTouch = touches[0].pageX;
|
||||
const touchY = touches[0].pageY;
|
||||
|
||||
// Only change when swipe is on header, we need horizontal scroll on tables
|
||||
if (touchY > 470) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (touches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentTouch < 50 ||
|
||||
isSidebarVisible ||
|
||||
isOrganizeModalOpen ||
|
||||
isEditMovieModalOpen ||
|
||||
isDeleteMovieModalOpen ||
|
||||
isManageMoviesModalOpen ||
|
||||
isInteractiveSearchModalOpen ||
|
||||
isMovieHistoryModalOpen
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
touchStart.current = currentTouch;
|
||||
},
|
||||
[
|
||||
isSidebarVisible,
|
||||
isOrganizeModalOpen,
|
||||
isEditMovieModalOpen,
|
||||
isDeleteMovieModalOpen,
|
||||
isManageMoviesModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
isMovieHistoryModalOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const handleTouchEnd = useCallback(
|
||||
(event: TouchEvent) => {
|
||||
const touches = event.changedTouches;
|
||||
const currentTouch = touches[0].pageX;
|
||||
|
||||
if (!touchStart.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentTouch > touchStart.current &&
|
||||
currentTouch - touchStart.current > 100 &&
|
||||
previousMovie !== undefined
|
||||
) {
|
||||
history.push(getPathWithUrlBase(`/movie/${previousMovie.titleSlug}`));
|
||||
} else if (
|
||||
currentTouch < touchStart.current &&
|
||||
touchStart.current - currentTouch > 100 &&
|
||||
nextMovie !== undefined
|
||||
) {
|
||||
history.push(getPathWithUrlBase(`/movie/${nextMovie.titleSlug}`));
|
||||
}
|
||||
|
||||
touchStart.current = null;
|
||||
},
|
||||
[previousMovie, nextMovie, history]
|
||||
);
|
||||
|
||||
const handleTouchCancel = useCallback(() => {
|
||||
touchStart.current = null;
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback(() => {
|
||||
if (!touchStart.current) {
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyUp = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.composedPath && event.composedPath().length === 4) {
|
||||
if (event.key === 'ArrowLeft' && previousMovie !== undefined) {
|
||||
history.push(getPathWithUrlBase(`/movie/${previousMovie.titleSlug}`));
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight' && nextMovie !== undefined) {
|
||||
history.push(getPathWithUrlBase(`/movie/${nextMovie.titleSlug}`));
|
||||
}
|
||||
}
|
||||
},
|
||||
[previousMovie, nextMovie, history]
|
||||
);
|
||||
|
||||
const populate = useCallback(() => {
|
||||
dispatch(fetchMovieFiles({ movieId }));
|
||||
dispatch(fetchExtraFiles({ movieId }));
|
||||
dispatch(fetchMovieCredits({ movieId }));
|
||||
dispatch(fetchQueueDetails({ movieId }));
|
||||
dispatch(fetchImportListSchema());
|
||||
dispatch(fetchRootFolders());
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
populate();
|
||||
}, [populate]);
|
||||
|
||||
useEffect(() => {
|
||||
registerPagePopulator(populate, ['movieUpdated']);
|
||||
|
||||
return () => {
|
||||
unregisterPagePopulator(populate);
|
||||
dispatch(clearMovieFiles());
|
||||
dispatch(clearExtraFiles());
|
||||
dispatch(clearMovieCredits());
|
||||
dispatch(clearQueueDetails());
|
||||
};
|
||||
}, [populate, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((!isRefreshing && wasRefreshing) || (!isRenaming && wasRenaming)) {
|
||||
populate();
|
||||
}
|
||||
}, [isRefreshing, wasRefreshing, isRenaming, wasRenaming, populate]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('touchstart', handleTouchStart);
|
||||
window.addEventListener('touchend', handleTouchEnd);
|
||||
window.addEventListener('touchcancel', handleTouchCancel);
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('touchstart', handleTouchStart);
|
||||
window.removeEventListener('touchend', handleTouchEnd);
|
||||
window.removeEventListener('touchcancel', handleTouchCancel);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, [
|
||||
handleTouchStart,
|
||||
handleTouchEnd,
|
||||
handleTouchCancel,
|
||||
handleTouchMove,
|
||||
handleKeyUp,
|
||||
]);
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
title,
|
||||
originalTitle,
|
||||
year,
|
||||
inCinemas,
|
||||
physicalRelease,
|
||||
digitalRelease,
|
||||
runtime,
|
||||
certification,
|
||||
ratings,
|
||||
path,
|
||||
statistics = {} as Statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
studio,
|
||||
originalLanguage,
|
||||
genres = [],
|
||||
collection,
|
||||
overview,
|
||||
status,
|
||||
youTubeTrailerId,
|
||||
isAvailable,
|
||||
images,
|
||||
tags,
|
||||
} = movie;
|
||||
|
||||
const { sizeOnDisk = 0 } = statistics;
|
||||
|
||||
const statusDetails = getMovieStatusDetails(status);
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const isFetching =
|
||||
isMovieFilesFetching || isExtraFilesFetching || isMovieCreditsFetching;
|
||||
|
||||
const marqueeWidth = isSmallScreen ? titleWidth : titleWidth - 150;
|
||||
|
||||
const titleWithYear = `${title}${year > 0 ? ` (${year})` : ''}`;
|
||||
|
||||
return (
|
||||
<PageContent title={titleWithYear}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshAndScan')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
title={translate('RefreshInformationAndScanDisk')}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={handleRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SearchMovie')}
|
||||
iconName={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
title={undefined}
|
||||
onPress={handleSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('InteractiveSearch')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
isSpinning={isSearching}
|
||||
title={undefined}
|
||||
onPress={handleInteractiveSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('PreviewRename')}
|
||||
iconName={icons.ORGANIZE}
|
||||
isDisabled={!hasMovieFiles}
|
||||
onPress={handleOrganizePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageFiles')}
|
||||
iconName={icons.MOVIE_FILE}
|
||||
onPress={handleManageMoviesPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('History')}
|
||||
iconName={icons.HISTORY}
|
||||
onPress={handleMovieHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Edit')}
|
||||
iconName={icons.EDIT}
|
||||
onPress={handleEditMoviePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Delete')}
|
||||
iconName={icons.DELETE}
|
||||
onPress={handleDeleteMoviePress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
style={
|
||||
fanartUrl ? { backgroundImage: `url(${fanartUrl})` } : undefined
|
||||
}
|
||||
>
|
||||
<div className={styles.backdropOverlay} />
|
||||
</div>
|
||||
|
||||
<div className={styles.headerContent}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={500}
|
||||
lazy={false}
|
||||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<div ref={titleRef} className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={40}
|
||||
onPress={handleMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.title} style={{ width: marqueeWidth }}>
|
||||
<Marquee text={title} title={originalTitle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.movieNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.movieNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={translate('MovieDetailsGoTo', {
|
||||
title: previousMovie.title,
|
||||
})}
|
||||
to={`/movie/${previousMovie.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.movieNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={translate('MovieDetailsGoTo', {
|
||||
title: nextMovie.title,
|
||||
})}
|
||||
to={`/movie/${nextMovie.titleSlug}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
{certification ? (
|
||||
<span
|
||||
className={styles.certification}
|
||||
title={translate('Certification')}
|
||||
>
|
||||
{certification}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className={styles.year}>
|
||||
<Popover
|
||||
anchor={
|
||||
year > 0 ? (
|
||||
year
|
||||
) : (
|
||||
<Icon
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
size={20}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={translate('ReleaseDates')}
|
||||
body={
|
||||
<MovieReleaseDates
|
||||
tmdbId={tmdbId}
|
||||
inCinemas={inCinemas}
|
||||
digitalRelease={digitalRelease}
|
||||
physicalRelease={physicalRelease}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{runtime ? (
|
||||
<span
|
||||
className={styles.runtime}
|
||||
title={translate('Runtime')}
|
||||
>
|
||||
{formatRuntime(runtime, movieRuntimeFormat)}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<span className={styles.links}>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={20} />}
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
imdbId={imdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{!!tags.length && (
|
||||
<span>
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.TAGS} size={20} />}
|
||||
tooltip={<MovieTags movieId={id} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
{ratings.tmdb ? (
|
||||
<span className={styles.rating}>
|
||||
<TmdbRating ratings={ratings} iconSize={20} />
|
||||
</span>
|
||||
) : null}
|
||||
{ratings.imdb ? (
|
||||
<span className={styles.rating}>
|
||||
<ImdbRating ratings={ratings} iconSize={20} />
|
||||
</span>
|
||||
) : null}
|
||||
{ratings.rottenTomatoes ? (
|
||||
<span className={styles.rating}>
|
||||
<RottenTomatoRating ratings={ratings} iconSize={20} />
|
||||
</span>
|
||||
) : null}
|
||||
{ratings.trakt ? (
|
||||
<span className={styles.rating}>
|
||||
<TraktRating ratings={ratings} iconSize={20} />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Path')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.path}>{path}</span>
|
||||
</InfoLabel>
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Status')}
|
||||
title={statusDetails.message}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.statusName}>
|
||||
<MovieStatusLabel
|
||||
movieId={id}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
hasMovieFiles={hasMovieFiles}
|
||||
status={status}
|
||||
/>
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('QualityProfile')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.qualityProfileName}>
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Size')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
|
||||
{collection ? (
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Collection')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div className={styles.collection}>
|
||||
<MovieCollectionLabel tmdbId={collection.tmdbId} />
|
||||
</div>
|
||||
</InfoLabel>
|
||||
) : null}
|
||||
|
||||
{originalLanguage?.name && !isSmallScreen ? (
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('OriginalLanguage')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.originalLanguage}>
|
||||
{originalLanguage.name}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
) : null}
|
||||
|
||||
{studio && !isSmallScreen ? (
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Studio')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.studio}>{studio}</span>
|
||||
</InfoLabel>
|
||||
) : null}
|
||||
|
||||
{genres.length && !isSmallScreen ? (
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
name={translate('Genres')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<MovieGenres className={styles.genres} genres={genres} />
|
||||
</InfoLabel>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div ref={overviewRef} className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(
|
||||
overviewHeight / (defaultFontSize * lineHeight)
|
||||
)}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{!isFetching && movieFilesError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingMovieFilesFailed')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && extraFilesError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingMovieExtraFilesFailed')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && movieCreditsError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('LoadingMovieCreditsFailed')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<FieldSet legend={translate('Files')}>
|
||||
<MovieFileEditorTable movieId={id} />
|
||||
|
||||
<ExtraFileTable movieId={id} />
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Cast')}>
|
||||
<MovieCastPosters isSmallScreen={isSmallScreen} />
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Crew')}>
|
||||
<MovieCrewPosters isSmallScreen={isSmallScreen} />
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Titles')}>
|
||||
<MovieTitlesTable movieId={id} />
|
||||
</FieldSet>
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModal
|
||||
isOpen={isOrganizeModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={handleOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={handleEditMovieModalClose}
|
||||
onDeleteMoviePress={handleDeleteMoviePress}
|
||||
/>
|
||||
|
||||
<MovieHistoryModal
|
||||
isOpen={isMovieHistoryModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={handleMovieHistoryModalClose}
|
||||
/>
|
||||
|
||||
<DeleteMovieModal
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={handleDeleteMovieModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isManageMoviesModalOpen}
|
||||
movieId={id}
|
||||
title={title}
|
||||
folder={path}
|
||||
initialSortKey="relativePath"
|
||||
initialSortDirection={sortDirections.ASCENDING}
|
||||
showMovie={false}
|
||||
allowMovieChange={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={translate('ManageFiles')}
|
||||
onModalClose={handleManageMoviesModalClose}
|
||||
/>
|
||||
|
||||
<MovieInteractiveSearchModal
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
onModalClose={handleInteractiveSearchModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieDetails;
|
|
@ -1,334 +0,0 @@
|
|||
import { push } from 'connected-react-router';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import MovieDetails from './MovieDetails';
|
||||
|
||||
const selectMovieFiles = createSelector(
|
||||
(state) => state.movieFiles,
|
||||
(movieFiles) => {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = movieFiles;
|
||||
|
||||
const hasMovieFiles = !!items.length;
|
||||
|
||||
return {
|
||||
isMovieFilesFetching: isFetching,
|
||||
isMovieFilesPopulated: isPopulated,
|
||||
movieFilesError: error,
|
||||
hasMovieFiles
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const selectMovieCredits = createSelector(
|
||||
(state) => state.movieCredits,
|
||||
(movieCredits) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = movieCredits;
|
||||
|
||||
return {
|
||||
isMovieCreditsFetching: isFetching,
|
||||
isMovieCreditsPopulated: isPopulated,
|
||||
movieCreditsError: error
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const selectExtraFiles = createSelector(
|
||||
(state) => state.extraFiles,
|
||||
(extraFiles) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = extraFiles;
|
||||
|
||||
return {
|
||||
isExtraFilesFetching: isFetching,
|
||||
isExtraFilesPopulated: isPopulated,
|
||||
extraFilesError: error
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { titleSlug }) => titleSlug,
|
||||
selectMovieFiles,
|
||||
selectMovieCredits,
|
||||
selectExtraFiles,
|
||||
createAllMoviesSelector(),
|
||||
createCommandsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.app.isSidebarVisible,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(titleSlug, movieFiles, movieCredits, extraFiles, allMovies, commands, dimensions, queueItems, isSidebarVisible, movieRuntimeFormat) => {
|
||||
const sortedMovies = _.orderBy(allMovies, 'sortTitle');
|
||||
const movieIndex = _.findIndex(sortedMovies, { titleSlug });
|
||||
const movie = sortedMovies[movieIndex];
|
||||
|
||||
if (!movie) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const {
|
||||
isMovieFilesFetching,
|
||||
isMovieFilesPopulated,
|
||||
movieFilesError,
|
||||
hasMovieFiles
|
||||
} = movieFiles;
|
||||
|
||||
const {
|
||||
isMovieCreditsFetching,
|
||||
isMovieCreditsPopulated,
|
||||
movieCreditsError
|
||||
} = movieCredits;
|
||||
|
||||
const {
|
||||
isExtraFilesFetching,
|
||||
isExtraFilesPopulated,
|
||||
extraFilesError
|
||||
} = extraFiles;
|
||||
|
||||
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
|
||||
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
|
||||
const isMovieRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_MOVIE, movieIds: [movie.id] }));
|
||||
const movieRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_MOVIE });
|
||||
const allMoviesRefreshing = (
|
||||
isCommandExecuting(movieRefreshingCommand) &&
|
||||
!movieRefreshingCommand.body.movieId
|
||||
);
|
||||
const isRefreshing = isMovieRefreshing || allMoviesRefreshing;
|
||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.MOVIE_SEARCH, movieIds: [movie.id] }));
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, movieId: movie.id }));
|
||||
const isRenamingMovieCommand = findCommand(commands, { name: commandNames.RENAME_MOVIE });
|
||||
const isRenamingMovie = (
|
||||
isCommandExecuting(isRenamingMovieCommand) &&
|
||||
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
|
||||
);
|
||||
|
||||
const isFetching = isMovieFilesFetching || isMovieCreditsFetching || isExtraFilesFetching;
|
||||
const isPopulated = isMovieFilesPopulated && isMovieCreditsPopulated && isExtraFilesPopulated;
|
||||
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
|
||||
acc.push(alternateTitle.title);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const queueItem = queueItems.find((item) => item.movieId === movie.id);
|
||||
|
||||
return {
|
||||
...movie,
|
||||
alternateTitles,
|
||||
isMovieRefreshing,
|
||||
allMoviesRefreshing,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
isRenamingFiles,
|
||||
isRenamingMovie,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
movieFilesError,
|
||||
movieCreditsError,
|
||||
extraFilesError,
|
||||
hasMovieFiles,
|
||||
previousMovie,
|
||||
nextMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
isSidebarVisible,
|
||||
queueItem,
|
||||
movieRuntimeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchMovieFiles({ movieId }) {
|
||||
dispatch(fetchMovieFiles({ movieId }));
|
||||
},
|
||||
dispatchClearMovieFiles() {
|
||||
dispatch(clearMovieFiles());
|
||||
},
|
||||
dispatchFetchMovieCredits({ movieId }) {
|
||||
dispatch(fetchMovieCredits({ movieId }));
|
||||
},
|
||||
dispatchClearMovieCredits() {
|
||||
dispatch(clearMovieCredits());
|
||||
},
|
||||
dispatchFetchExtraFiles({ movieId }) {
|
||||
dispatch(fetchExtraFiles({ movieId }));
|
||||
},
|
||||
dispatchClearExtraFiles() {
|
||||
dispatch(clearExtraFiles());
|
||||
},
|
||||
dispatchFetchQueueDetails({ movieId }) {
|
||||
dispatch(fetchQueueDetails({ movieId }));
|
||||
},
|
||||
dispatchClearQueueDetails() {
|
||||
dispatch(clearQueueDetails());
|
||||
},
|
||||
dispatchFetchImportListSchema() {
|
||||
dispatch(fetchImportListSchema());
|
||||
},
|
||||
dispatchToggleMovieMonitored(payload) {
|
||||
dispatch(toggleMovieMonitored(payload));
|
||||
},
|
||||
dispatchExecuteCommand(payload) {
|
||||
dispatch(executeCommand(payload));
|
||||
},
|
||||
onGoToMovie(titleSlug) {
|
||||
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class MovieDetailsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.populate, ['movieUpdated']);
|
||||
this.populate();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
isMovieRefreshing,
|
||||
allMoviesRefreshing,
|
||||
isRenamingFiles,
|
||||
isRenamingMovie
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(prevProps.isMovieRefreshing && !isMovieRefreshing) ||
|
||||
(prevProps.allMoviesRefreshing && !allMoviesRefreshing) ||
|
||||
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||
(prevProps.isRenamingMovie && !isRenamingMovie)
|
||||
) {
|
||||
this.populate();
|
||||
}
|
||||
|
||||
// If the id has changed we need to clear the episodes/episode
|
||||
// files and fetch from the server.
|
||||
|
||||
if (prevProps.id !== id) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.populate);
|
||||
this.unpopulate();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
populate = () => {
|
||||
const movieId = this.props.id;
|
||||
|
||||
this.props.dispatchFetchMovieFiles({ movieId });
|
||||
this.props.dispatchFetchExtraFiles({ movieId });
|
||||
this.props.dispatchFetchMovieCredits({ movieId });
|
||||
this.props.dispatchFetchQueueDetails({ movieId });
|
||||
this.props.dispatchFetchImportListSchema();
|
||||
};
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.dispatchClearMovieFiles();
|
||||
this.props.dispatchClearExtraFiles();
|
||||
this.props.dispatchClearMovieCredits();
|
||||
this.props.dispatchClearQueueDetails();
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.dispatchToggleMovieMonitored({
|
||||
movieId: this.props.id,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.REFRESH_MOVIE,
|
||||
movieIds: [this.props.id]
|
||||
});
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: [this.props.id]
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieDetails
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieDetailsConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
isMovieRefreshing: PropTypes.bool.isRequired,
|
||||
allMoviesRefreshing: PropTypes.bool.isRequired,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingMovie: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
dispatchFetchMovieFiles: PropTypes.func.isRequired,
|
||||
dispatchClearMovieFiles: PropTypes.func.isRequired,
|
||||
dispatchFetchExtraFiles: PropTypes.func.isRequired,
|
||||
dispatchClearExtraFiles: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieCredits: PropTypes.func.isRequired,
|
||||
dispatchClearMovieCredits: PropTypes.func.isRequired,
|
||||
dispatchToggleMovieMonitored: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchClearQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchImportListSchema: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
onGoToMovie: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieDetailsConnector);
|
39
frontend/src/Movie/Details/MovieDetailsPage.tsx
Normal file
39
frontend/src/Movie/Details/MovieDetailsPage.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieDetails from './MovieDetails';
|
||||
|
||||
function MovieDetailsPage() {
|
||||
const allMovies = useSelector(createAllMoviesSelector());
|
||||
const { titleSlug } = useParams<{ titleSlug: string }>();
|
||||
const history = useHistory();
|
||||
|
||||
const movieIndex = allMovies.findIndex(
|
||||
(movie) => movie.titleSlug === titleSlug
|
||||
);
|
||||
|
||||
const previousIndex = usePrevious(movieIndex);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
movieIndex === -1 &&
|
||||
previousIndex !== -1 &&
|
||||
previousIndex !== undefined
|
||||
) {
|
||||
history.push(`${window.Radarr.urlBase}/`);
|
||||
}
|
||||
}, [movieIndex, previousIndex, history]);
|
||||
|
||||
if (movieIndex === -1) {
|
||||
return <NotFound message={translate('MovieCannotBeFound')} />;
|
||||
}
|
||||
|
||||
return <MovieDetails movieId={allMovies[movieIndex].id} />;
|
||||
}
|
||||
|
||||
export default MovieDetailsPage;
|
|
@ -1,125 +0,0 @@
|
|||
import { push } from 'connected-react-router';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieDetailsConnector from './MovieDetailsConnector';
|
||||
import styles from './MovieDetails.css';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { match }) => match,
|
||||
(state) => state.movies,
|
||||
(match, movies) => {
|
||||
const titleSlug = match.params.titleSlug;
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = movies;
|
||||
|
||||
const movieIndex = _.findIndex(items, { titleSlug });
|
||||
|
||||
if (movieIndex > -1) {
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
titleSlug
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
push,
|
||||
fetchRootFolders
|
||||
};
|
||||
|
||||
class MovieDetailsPageConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.props.titleSlug) {
|
||||
this.props.push(`${window.Radarr.urlBase}/`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
titleSlug,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = this.props;
|
||||
|
||||
if (isFetching && !isPopulated) {
|
||||
return (
|
||||
<PageContent title={translate('Loading')}>
|
||||
<PageContentBody>
|
||||
<LoadingIndicator />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<div className={styles.errorMessage}>
|
||||
{getErrorMessage(error, translate('FailedToLoadMovieFromAPI'))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!titleSlug) {
|
||||
return (
|
||||
<NotFound
|
||||
message={translate('SorryThatMovieCannotBeFound')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MovieDetailsConnector
|
||||
titleSlug={titleSlug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieDetailsPageConnector.propTypes = {
|
||||
titleSlug: PropTypes.string,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
|
||||
push: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieDetailsPageConnector);
|
|
@ -1,13 +1,23 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { MovieStatus } from 'Movie/Movie';
|
||||
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||
import Queue from 'typings/Queue';
|
||||
import getQueueStatusText from 'Utilities/Movie/getQueueStatusText';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieStatusLabel.css';
|
||||
|
||||
function getMovieStatus(status, hasFile, isMonitored, isAvailable, queueItem = false) {
|
||||
function getMovieStatus(
|
||||
status: MovieStatus,
|
||||
isMonitored: boolean,
|
||||
isAvailable: boolean,
|
||||
hasFiles: boolean,
|
||||
queueItem: Queue | null = null
|
||||
) {
|
||||
if (queueItem) {
|
||||
const queueStatus = queueItem.status;
|
||||
const queueState = queueItem.trackedDownloadStatus;
|
||||
|
@ -18,11 +28,11 @@ function getMovieStatus(status, hasFile, isMonitored, isAvailable, queueItem = f
|
|||
}
|
||||
}
|
||||
|
||||
if (hasFile && !isMonitored) {
|
||||
if (hasFiles && !isMonitored) {
|
||||
return 'availNotMonitored';
|
||||
}
|
||||
|
||||
if (hasFile) {
|
||||
if (hasFiles) {
|
||||
return 'ended';
|
||||
}
|
||||
|
||||
|
@ -30,34 +40,52 @@ function getMovieStatus(status, hasFile, isMonitored, isAvailable, queueItem = f
|
|||
return 'deleted';
|
||||
}
|
||||
|
||||
if (isAvailable && !isMonitored && !hasFile) {
|
||||
if (isAvailable && !isMonitored && !hasFiles) {
|
||||
return 'missingUnmonitored';
|
||||
}
|
||||
|
||||
if (isAvailable && !hasFile) {
|
||||
if (isAvailable && !hasFiles) {
|
||||
return 'missingMonitored';
|
||||
}
|
||||
|
||||
return 'continuing';
|
||||
}
|
||||
|
||||
function MovieStatusLabel(props) {
|
||||
const {
|
||||
interface MovieStatusLabelProps {
|
||||
movieId: number;
|
||||
monitored: boolean;
|
||||
isAvailable: boolean;
|
||||
hasMovieFiles: boolean;
|
||||
status: MovieStatus;
|
||||
useLabel?: boolean;
|
||||
}
|
||||
|
||||
function MovieStatusLabel({
|
||||
movieId,
|
||||
monitored,
|
||||
isAvailable,
|
||||
hasMovieFiles,
|
||||
status,
|
||||
useLabel = false,
|
||||
}: MovieStatusLabelProps) {
|
||||
const queueItem = useSelector(createQueueItemSelectorForHook(movieId));
|
||||
|
||||
let movieStatus = getMovieStatus(
|
||||
status,
|
||||
hasMovieFiles,
|
||||
monitored,
|
||||
isAvailable,
|
||||
queueItem,
|
||||
useLabel,
|
||||
colorImpairedMode
|
||||
} = props;
|
||||
hasMovieFiles,
|
||||
queueItem
|
||||
);
|
||||
|
||||
let movieStatus = getMovieStatus(status, hasMovieFiles, monitored, isAvailable, queueItem);
|
||||
let statusClass = movieStatus;
|
||||
|
||||
if (movieStatus === 'availNotMonitored' || movieStatus === 'ended') {
|
||||
movieStatus = 'downloaded';
|
||||
} else if (movieStatus === 'missingMonitored' || movieStatus === 'missingUnmonitored') {
|
||||
} else if (
|
||||
movieStatus === 'missingMonitored' ||
|
||||
movieStatus === 'missingUnmonitored'
|
||||
) {
|
||||
movieStatus = 'missing';
|
||||
} else if (movieStatus === 'continuing') {
|
||||
movieStatus = 'notAvailable';
|
||||
|
@ -68,7 +96,7 @@ function MovieStatusLabel(props) {
|
|||
}
|
||||
|
||||
if (useLabel) {
|
||||
let kind = kinds.SUCCESS;
|
||||
let kind: Kind = kinds.SUCCESS;
|
||||
|
||||
switch (statusClass) {
|
||||
case 'queue':
|
||||
|
@ -93,11 +121,7 @@ function MovieStatusLabel(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
kind={kind}
|
||||
size={sizes.LARGE}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
>
|
||||
<Label kind={kind} size={sizes.LARGE}>
|
||||
{translate(firstCharToUpper(movieStatus))}
|
||||
</Label>
|
||||
);
|
||||
|
@ -105,6 +129,8 @@ function MovieStatusLabel(props) {
|
|||
|
||||
return (
|
||||
<span
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
className={styles[statusClass]}
|
||||
>
|
||||
{translate(firstCharToUpper(movieStatus))}
|
||||
|
@ -112,19 +138,4 @@ function MovieStatusLabel(props) {
|
|||
);
|
||||
}
|
||||
|
||||
MovieStatusLabel.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
hasMovieFiles: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
queueItem: PropTypes.object,
|
||||
useLabel: PropTypes.bool,
|
||||
colorImpairedMode: PropTypes.bool
|
||||
};
|
||||
|
||||
MovieStatusLabel.defaultProps = {
|
||||
useLabel: false,
|
||||
colorImpairedMode: false
|
||||
};
|
||||
|
||||
export default MovieStatusLabel;
|
|
@ -1,30 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
|
||||
function MovieTags({ tags }) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
tags.map((tag) => {
|
||||
return (
|
||||
<Label
|
||||
key={tag}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{tag}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieTags.propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
};
|
||||
|
||||
export default MovieTags;
|
35
frontend/src/Movie/Details/MovieTags.tsx
Normal file
35
frontend/src/Movie/Details/MovieTags.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import useTags from 'Tags/useTags';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
|
||||
interface MovieTagsProps {
|
||||
movieId: number;
|
||||
}
|
||||
|
||||
function MovieTags({ movieId }: MovieTagsProps) {
|
||||
const movie = useMovie(movieId)!;
|
||||
const tagList = useTags();
|
||||
|
||||
const tags = movie.tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'))
|
||||
.map((tag) => tag.label);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<Label key={tag} kind={kinds.INFO} size={sizes.LARGE}>
|
||||
{tag}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieTags;
|
|
@ -1,26 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import MovieTags from './MovieTags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createTagsSelector(),
|
||||
(movie, tagList) => {
|
||||
const tags = movie.tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'))
|
||||
.map((tag) => tag.label);
|
||||
|
||||
return {
|
||||
tags
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(MovieTags);
|
|
@ -1,53 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SearchMenuItem from 'Components/Menu/SearchMenuItem';
|
||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class MovieIndexSearchMenu extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isDisabled,
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
isDisabled={isDisabled}
|
||||
alignMenu={align.RIGHT}
|
||||
>
|
||||
<ToolbarMenuButton
|
||||
iconName={icons.SEARCH}
|
||||
text="Search"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<MenuContent>
|
||||
<SearchMenuItem
|
||||
name="missingMoviesSearch"
|
||||
onPress={onSearchPress}
|
||||
>
|
||||
{translate('SearchMissing')}
|
||||
</SearchMenuItem>
|
||||
|
||||
<SearchMenuItem
|
||||
name="cutoffUnmetMoviesSearch"
|
||||
onPress={onSearchPress}
|
||||
>
|
||||
{translate('SearchCutoffUnmet')}
|
||||
</SearchMenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieIndexSearchMenu.propTypes = {
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndexSearchMenu;
|
|
@ -14,13 +14,15 @@ function createMovieIndexItemSelector(movieId: number) {
|
|||
(movie: Movie, qualityProfile, executingCommands: Command[]) => {
|
||||
const isRefreshingMovie = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === REFRESH_MOVIE && command.body.movieId === movieId
|
||||
command.name === REFRESH_MOVIE &&
|
||||
command.body.movieIds?.includes(movieId)
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingMovie = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === MOVIE_SEARCH && command.body.movieId === movieId
|
||||
command.name === MOVIE_SEARCH &&
|
||||
command.body.movieIds?.includes(movieId)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ export default {
|
|||
MOVIES,
|
||||
INTERACTIVE_IMPORT,
|
||||
WANTED_CUTOFF_UNMET,
|
||||
WANTED_MISSING
|
||||
};
|
||||
WANTED_MISSING,
|
||||
} as const;
|
|
@ -1 +1,11 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export type ExtraFileType = 'subtitle' | 'metadata' | 'other';
|
||||
|
||||
export interface ExtraFile extends ModelBase {
|
||||
movieId: number;
|
||||
movieFileId?: number;
|
||||
relativePath: string;
|
||||
extension: string;
|
||||
type: ExtraFileType;
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function createHealthCheckSelector() {
|
||||
return createSelector(
|
||||
(state) => state.system.health,
|
||||
(state) => state.app,
|
||||
(health, app) => {
|
||||
const items = [...health.items];
|
||||
|
||||
if (!app.isConnected) {
|
||||
items.push({
|
||||
source: 'UI',
|
||||
type: 'warning',
|
||||
message: translate('CouldNotConnectSignalR'),
|
||||
wikiUrl: 'https://wiki.servarr.com/radarr/system#could-not-connect-to-signalr'
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createHealthCheckSelector;
|
8
frontend/src/Tags/useTags.ts
Normal file
8
frontend/src/Tags/useTags.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
|
||||
const useTags = () => {
|
||||
return useSelector(createTagsSelector());
|
||||
};
|
||||
|
||||
export default useTags;
|
|
@ -407,7 +407,7 @@
|
|||
"GrabRelease": "انتزاع الإصدار",
|
||||
"Grabbed": "اقتطف",
|
||||
"Grab": "إختطاف",
|
||||
"GoToInterp": "انتقل إلى {0}",
|
||||
"MovieDetailsGoTo": "انتقل إلى {0}",
|
||||
"Global": "عالمي",
|
||||
"Genres": "الأنواع",
|
||||
"GeneralSettingsSummary": "المنفذ ، SSL ، اسم المستخدم / كلمة المرور ، الوكيل ، التحليلات والتحديثات",
|
||||
|
@ -516,7 +516,7 @@
|
|||
"SourcePath": "مسار المصدر",
|
||||
"Source": "مصدر",
|
||||
"Sort": "فرز",
|
||||
"SorryThatMovieCannotBeFound": "آسف ، لا يمكن العثور على هذا الفيلم.",
|
||||
"MovieCannotBeFound": "آسف ، لا يمكن العثور على هذا الفيلم.",
|
||||
"SomeResultsHiddenFilter": "بعض النتائج مخفية بواسطة عامل التصفية المطبق",
|
||||
"Socks5": "Socks5 (دعم TOR)",
|
||||
"Socks4": "الجوارب 4",
|
||||
|
|
|
@ -253,7 +253,7 @@
|
|||
"GeneralSettingsSummary": "Порт, SSL, потребителско име / парола, прокси, анализи и актуализации",
|
||||
"Genres": "Жанрове",
|
||||
"Global": "Глобален",
|
||||
"GoToInterp": "Отидете на {0}",
|
||||
"MovieDetailsGoTo": "Отидете на {0}",
|
||||
"Grab": "Грабнете",
|
||||
"Grabbed": "Грабната",
|
||||
"GrabRelease": "Grab Release",
|
||||
|
@ -532,7 +532,7 @@
|
|||
"Socks4": "Чорапи4",
|
||||
"Socks5": "Чорапи5 (Поддръжка на TOR)",
|
||||
"SomeResultsHiddenFilter": "Някои резултати са скрити от прилагания филтър",
|
||||
"SorryThatMovieCannotBeFound": "За съжаление този филм не може да бъде намерен.",
|
||||
"MovieCannotBeFound": "За съжаление този филм не може да бъде намерен.",
|
||||
"Sort": "Вид",
|
||||
"Source": "Източник",
|
||||
"SourcePath": "Път на източника",
|
||||
|
|
|
@ -511,7 +511,7 @@
|
|||
"General": "General",
|
||||
"GeneralSettings": "Configuració general",
|
||||
"Global": "Global",
|
||||
"GoToInterp": "Vés a {0}",
|
||||
"MovieDetailsGoTo": "Vés a {0}",
|
||||
"Grab": "Captura",
|
||||
"Grabbed": "Capturat",
|
||||
"ICalFeedHelpText": "Copieu aquest URL als vostres clients o feu clic per a subscriure's si el vostre navegador és compatible amb webcal",
|
||||
|
@ -965,7 +965,7 @@
|
|||
"SetPermissions": "Estableix permisos",
|
||||
"Small": "Petita",
|
||||
"SomeResultsHiddenFilter": "Alguns resultats estan ocults pel filtre aplicat",
|
||||
"SorryThatMovieCannotBeFound": "Ho sentim, aquesta pel·lícula no s'ha trobat.",
|
||||
"MovieCannotBeFound": "Ho sentim, aquesta pel·lícula no s'ha trobat.",
|
||||
"Sort": "Ordena",
|
||||
"Source": "Font",
|
||||
"SourcePath": "Camí de la font",
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
"SupportedDownloadClientsMoreInfo": "Další informace o jednotlivých klientech pro stahování získáte kliknutím na informační tlačítka.",
|
||||
"SupportedListsMoreInfo": "Další informace o jednotlivých seznamech importů získáte kliknutím na informační tlačítka.",
|
||||
"GeneralSettingsSummary": "Port, SSL, uživatelské jméno / heslo, proxy, analytika a aktualizace",
|
||||
"GoToInterp": "Přejít na {0}",
|
||||
"MovieDetailsGoTo": "Přejít na {0}",
|
||||
"Interval": "Interval",
|
||||
"LanguageHelpText": "Jazyk pro zprávy",
|
||||
"LastWriteTime": "Čas posledního zápisu",
|
||||
|
@ -832,7 +832,7 @@
|
|||
"Socks4": "4. ponožky",
|
||||
"Socks5": "Ponožky5 (podpora TOR)",
|
||||
"SomeResultsHiddenFilter": "Některé výsledky jsou použitým filtrem skryty",
|
||||
"SorryThatMovieCannotBeFound": "Je nám líto, ale tento film nelze najít.",
|
||||
"MovieCannotBeFound": "Je nám líto, ale tento film nelze najít.",
|
||||
"Sort": "Třídit",
|
||||
"SourcePath": "Cesta zdroje",
|
||||
"SourceRelativePath": "Cesta relativního zdroje",
|
||||
|
|
|
@ -578,7 +578,7 @@
|
|||
"SupportedIndexersMoreInfo": "Klik på info-knapperne for at få flere oplysninger om de enkelte indeksatorer.",
|
||||
"GeneralSettings": "Generelle indstillinger",
|
||||
"Global": "Global",
|
||||
"GoToInterp": "Gå til {0}",
|
||||
"MovieDetailsGoTo": "Gå til {0}",
|
||||
"Grab": "Tag fat",
|
||||
"GrabRelease": "Hent udgivelse",
|
||||
"GrabReleaseMessageText": "{appName} var ikke i stand til at bestemme, hvilken film denne udgivelse var til. {appName} kan muligvis ikke automatisk importere denne udgivelse. Vil du hente '{0}'?",
|
||||
|
@ -807,7 +807,7 @@
|
|||
"Socks4": "Strømper 4",
|
||||
"Socks5": "Socks5 (Understøtter TOR)",
|
||||
"SomeResultsHiddenFilter": "Nogle resultater skjules af det anvendte filter",
|
||||
"SorryThatMovieCannotBeFound": "Beklager, den film kan ikke findes.",
|
||||
"MovieCannotBeFound": "Beklager, den film kan ikke findes.",
|
||||
"Sort": "Sortere",
|
||||
"Source": "Kilde",
|
||||
"SourcePath": "Kildesti",
|
||||
|
|
|
@ -483,7 +483,7 @@
|
|||
"ShowTitleHelpText": "Filmtitel unter dem Plakat anzeigen",
|
||||
"ShowUnknownMovieItems": "Unzugeordente Filmeinträge anzeigen",
|
||||
"SkipFreeSpaceCheck": "Prüfung des freien Speichers überspringen",
|
||||
"SorryThatMovieCannotBeFound": "Schade, dieser Film kann nicht gefunden werden.",
|
||||
"MovieCannotBeFound": "Schade, dieser Film kann nicht gefunden werden.",
|
||||
"SourcePath": "Quellpfad",
|
||||
"SourceRelativePath": "Relativer Quellpfad",
|
||||
"SslCertPassword": "SSL Zertifikat Passwort",
|
||||
|
@ -610,7 +610,7 @@
|
|||
"ImportFailed": "Import fehlgeschlagen: {0}",
|
||||
"HiddenClickToShow": "Versteckt, zum Anzeigen anklicken",
|
||||
"GrabReleaseMessageText": "{appName} konnte nicht bestimmen, für welchen Film dieser Release ist. {appName} könnte diesen Release möglicherweise nicht automatisch importieren. Möchtest du '{0}' grabben?",
|
||||
"GoToInterp": "Zu {0} gehen",
|
||||
"MovieDetailsGoTo": "Zu {0} gehen",
|
||||
"ExistingTag": "Vorhandener Tag",
|
||||
"ExcludeMovie": "Film ausschließen",
|
||||
"SearchIsNotSupportedWithThisIndexer": "Suche wird von diesem Indexer nicht unterstützt",
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
"ProxyPasswordHelpText": "Πρέπει να εισαγάγετε ένα όνομα χρήστη και έναν κωδικό πρόσβασης μόνο εάν απαιτείται. Αφήστε τα κενά διαφορετικά.",
|
||||
"RemovedFromTaskQueue": "Καταργήθηκε από την ουρά εργασιών",
|
||||
"ReplaceIllegalCharactersHelpText": "Αντικαταστήστε τους παράνομους χαρακτήρες. Εάν δεν είναι επιλεγμένο, το {appName} θα τα καταργήσει",
|
||||
"SorryThatMovieCannotBeFound": "Δυστυχώς, δεν είναι δυνατή η εύρεση αυτής της ταινίας.",
|
||||
"MovieCannotBeFound": "Δυστυχώς, δεν είναι δυνατή η εύρεση αυτής της ταινίας.",
|
||||
"Source": "Πηγή",
|
||||
"ICalTagsMoviesHelpText": "Ισχύει για ταινίες με τουλάχιστον μία αντίστοιχη ετικέτα",
|
||||
"TheLogLevelDefault": "Το επίπεδο καταγραφής είναι από προεπιλογή σε «Πληροφορίες» και μπορεί να αλλάξει",
|
||||
|
@ -557,7 +557,7 @@
|
|||
"SupportedIndexersMoreInfo": "Για περισσότερες πληροφορίες σχετικά με τους μεμονωμένους δείκτες, κάντε κλικ στα κουμπιά πληροφοριών.",
|
||||
"GeneralSettings": "Γενικές Ρυθμίσεις",
|
||||
"Global": "Παγκόσμια",
|
||||
"GoToInterp": "Μετάβαση στο {0}",
|
||||
"MovieDetailsGoTo": "Μετάβαση στο {0}",
|
||||
"Grab": "Αρπάζω",
|
||||
"Grabbed": "Αρπαξε",
|
||||
"GrabRelease": "Πιάσε την απελευθέρωση",
|
||||
|
|
|
@ -651,7 +651,6 @@
|
|||
"FailedDownloadHandling": "Failed Download Handling",
|
||||
"FailedLoadingSearchResults": "Failed to load search results, please try again.",
|
||||
"FailedToFetchUpdates": "Failed to fetch updates",
|
||||
"FailedToLoadMovieFromAPI": "Failed to load movie from API",
|
||||
"FailedToUpdateSettings": "Failed to update settings",
|
||||
"Fallback": "Fallback",
|
||||
"False": "False",
|
||||
|
@ -703,7 +702,6 @@
|
|||
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics and updates",
|
||||
"Genres": "Genres",
|
||||
"Global": "Global",
|
||||
"GoToInterp": "Go to {0}",
|
||||
"Grab": "Grab",
|
||||
"GrabId": "Grab ID",
|
||||
"GrabRelease": "Grab Release",
|
||||
|
@ -1042,9 +1040,11 @@
|
|||
"Movie": "Movie",
|
||||
"MovieAlreadyExcluded": "Movie already Excluded",
|
||||
"MovieAndCollection": "Movie and Collection",
|
||||
"MovieCannotBeFound": "Sorry, that movie cannot be found.",
|
||||
"MovieChat": "Movie Chat",
|
||||
"MovieCollectionFolderMultipleMissingRootsHealthCheckMessage": "Multiple root folders are missing for movie collections: {rootFoldersInfo}",
|
||||
"MovieCollectionRootFolderMissingRootHealthCheckMessage": "Missing root folder for movie collection: {rootFolderInfo}",
|
||||
"MovieDetailsGoTo": "Go to {0}",
|
||||
"MovieDetailsNextMovie": "Movie Details: Next Movie",
|
||||
"MovieDetailsPreviousMovie": "Movie Details: Previous Movie",
|
||||
"MovieDownloaded": "Movie Downloaded",
|
||||
|
@ -1735,7 +1735,6 @@
|
|||
"Socks4": "Socks4",
|
||||
"Socks5": "Socks5 (Support TOR)",
|
||||
"SomeResultsHiddenFilter": "Some results are hidden by the applied filter",
|
||||
"SorryThatMovieCannotBeFound": "Sorry, that movie cannot be found.",
|
||||
"Sort": "Sort",
|
||||
"Source": "Source",
|
||||
"SourcePath": "Source Path",
|
||||
|
|
|
@ -422,7 +422,7 @@
|
|||
"SslCertPassword": "Contraseña del Certificado SSL",
|
||||
"SourceRelativePath": "Ruta relativa de la fuente",
|
||||
"SourcePath": "Ruta de la fuente",
|
||||
"SorryThatMovieCannotBeFound": "Lo siento, no he encontrado esa película.",
|
||||
"MovieCannotBeFound": "Lo siento, no he encontrado esa película.",
|
||||
"SkipFreeSpaceCheck": "Saltar comprobación de espacio libre",
|
||||
"ShowUnknownMovieItems": "Mostrar Elementos Desconocidos",
|
||||
"ShowTitleHelpText": "Mostrar el título de la película debajo del poster",
|
||||
|
@ -611,7 +611,7 @@
|
|||
"ImportFailed": "Error de importación: {sourceTitle}",
|
||||
"HiddenClickToShow": "Oculto, pulsa para mostrar",
|
||||
"GrabReleaseMessageText": "{appName} no pudo determinar para qué película es este lanzamiento. {appName} no podrá importar este lanzamiento automáticamente. ¿Quieres descargar '{0}'?",
|
||||
"GoToInterp": "Ir a {0}",
|
||||
"MovieDetailsGoTo": "Ir a {0}",
|
||||
"ExistingTag": "Etiquetas existentes",
|
||||
"ExcludeMovie": "Excluir Película",
|
||||
"SearchIsNotSupportedWithThisIndexer": "La búsqueda no está soportada con este indexador",
|
||||
|
|
|
@ -566,7 +566,7 @@
|
|||
"GeneralSettings": "Yleiset asetukset",
|
||||
"GeneralSettingsSummary": "Portti, SSL-salaus, käyttäjätunnus ja salasana, välityspalvelin, analytiikka ja päivitykset.",
|
||||
"Global": "Yleiset",
|
||||
"GoToInterp": "Siirry kohteeseen '{0}'",
|
||||
"MovieDetailsGoTo": "Siirry kohteeseen '{0}'",
|
||||
"Grab": "Kaappaa",
|
||||
"Grabbed": "Kaapattu",
|
||||
"GrabRelease": "Kaappaa julkaisu",
|
||||
|
@ -811,7 +811,7 @@
|
|||
"Socks4": "SOCKS4",
|
||||
"Socks5": "SOCKS5 (TOR-tuki)",
|
||||
"SomeResultsHiddenFilter": "Aktiivinen suodatin piilottaa joitakin tuloksia.",
|
||||
"SorryThatMovieCannotBeFound": "Valitettavasti elokuvaa ei löydy.",
|
||||
"MovieCannotBeFound": "Valitettavasti elokuvaa ei löydy.",
|
||||
"Sort": "Järjestys",
|
||||
"Source": "Lähde",
|
||||
"SourcePath": "Lähdesijainti",
|
||||
|
|
|
@ -424,7 +424,7 @@
|
|||
"DeleteRestrictionHelpText": "Voulez-vous vraiment supprimer cette restriction ?",
|
||||
"DeleteIndexerMessageText": "Voulez-vous vraiment supprimer l'indexeur « {name} » ?",
|
||||
"CopyToClipboard": "Copier dans le presse-papier",
|
||||
"GoToInterp": "Aller à {0}",
|
||||
"MovieDetailsGoTo": "Aller à {0}",
|
||||
"SupportedListsMoreInfo": "Pour plus d'informations sur les listes d'importation individuelles, cliquez sur les boutons d'information.",
|
||||
"SupportedDownloadClientsMoreInfo": "Pour plus d'informations sur chaque client de téléchargement, cliquez sur les boutons plus d'information.",
|
||||
"FilterPlaceHolder": "Chercher les films",
|
||||
|
@ -606,7 +606,7 @@
|
|||
"SslCertPassword": "Mot de passe du certificat SSL",
|
||||
"SourceRelativePath": "Chemin relatif de la source",
|
||||
"SourcePath": "Chemin source",
|
||||
"SorryThatMovieCannotBeFound": "Désolé, ce film est introuvable.",
|
||||
"MovieCannotBeFound": "Désolé, ce film est introuvable.",
|
||||
"SkipFreeSpaceCheck": "Ignorer la vérification de l'espace libre",
|
||||
"ShowYear": "Afficher l'année",
|
||||
"ShowUnknownMovieItems": "Afficher les éléments de film inconnus",
|
||||
|
|
|
@ -563,7 +563,7 @@
|
|||
"GeneralSettingsSummary": "יציאה, SSL, שם משתמש / סיסמה, פרוקסי, ניתוחים ועדכונים",
|
||||
"Genres": "ז'אנרים",
|
||||
"Global": "גלוֹבָּלִי",
|
||||
"GoToInterp": "עבור אל {0}",
|
||||
"MovieDetailsGoTo": "עבור אל {0}",
|
||||
"Grab": "לִתְפּוֹס",
|
||||
"Grabbed": "תפס",
|
||||
"GrabRelease": "שחרור תפוס",
|
||||
|
@ -812,7 +812,7 @@
|
|||
"Socks4": "גרביים 4",
|
||||
"Socks5": "Socks5 (תמיכה ב- TOR)",
|
||||
"SomeResultsHiddenFilter": "חלק מהתוצאות מוסתרות על ידי המסנן שהוחל",
|
||||
"SorryThatMovieCannotBeFound": "מצטערים, הסרט הזה לא נמצא.",
|
||||
"MovieCannotBeFound": "מצטערים, הסרט הזה לא נמצא.",
|
||||
"Sort": "סוג",
|
||||
"SourcePath": "נתיב מקור",
|
||||
"SourceRelativePath": "נתיב יחסי מקור",
|
||||
|
|
|
@ -700,7 +700,7 @@
|
|||
"GeneralSettings": "सामान्य सेटिंग्स",
|
||||
"Genres": "शैलियां",
|
||||
"Global": "वैश्विक",
|
||||
"GoToInterp": "{0} पर जाएं",
|
||||
"MovieDetailsGoTo": "{0} पर जाएं",
|
||||
"Grab": "लपकना",
|
||||
"Grabbed": "पकड़ा",
|
||||
"GrabRelease": "पकड़ो रिलीज",
|
||||
|
@ -861,7 +861,7 @@
|
|||
"SkipFreeSpaceCheck": "फ्री स्पेस चेक छोड़ें",
|
||||
"Small": "छोटा",
|
||||
"SomeResultsHiddenFilter": "कुछ परिणाम लागू फ़िल्टर द्वारा छिपे हुए हैं",
|
||||
"SorryThatMovieCannotBeFound": "क्षमा करें, वह फिल्म नहीं मिल रही है।",
|
||||
"MovieCannotBeFound": "क्षमा करें, वह फिल्म नहीं मिल रही है।",
|
||||
"Sort": "तरह",
|
||||
"Source": "स्रोत",
|
||||
"SourcePath": "स्रोत पथ",
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
"HardlinkCopyFiles": "Hardlinkelés/Fájl(ok) Másolása",
|
||||
"Group": "Csoport",
|
||||
"GrabReleaseMessageText": "{appName} nem tudta meghatározni, hogy melyik filmhez készült ez a kiadás. Lehet, hogy a {appName} nem tudja automatikusan importálni ezt a kiadást. Meg szeretnéd ragadni a (z) „{0}”-t?",
|
||||
"GoToInterp": "Ugrás ide: {0}",
|
||||
"MovieDetailsGoTo": "Ugrás ide: {0}",
|
||||
"Genres": "Műfajok",
|
||||
"GeneralSettingsSummary": "Port, SSL, felhasználónév/jelszó, proxy, elemzések és frissítések",
|
||||
"FreeSpace": "Szabad hely",
|
||||
|
@ -258,7 +258,7 @@
|
|||
"SourcePath": "Forrás útvonala",
|
||||
"Source": "Forrás",
|
||||
"Sort": "Fajta",
|
||||
"SorryThatMovieCannotBeFound": "Sajnáljuk, ez a film nem található.",
|
||||
"MovieCannotBeFound": "Sajnáljuk, ez a film nem található.",
|
||||
"SkipFreeSpaceCheck": "Kihagyja a szabad hely ellenőrzését",
|
||||
"SizeOnDisk": "Méret a lemezen",
|
||||
"Size": "Méret",
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
"ShowUnknownMovieItems": "Sýna óþekktar kvikmyndir",
|
||||
"SkipFreeSpaceCheck": "Slepptu ókeypis plássathugun",
|
||||
"Socks5": "Socks5 (stuðningur TOR)",
|
||||
"SorryThatMovieCannotBeFound": "Því miður er ekki hægt að finna þá kvikmynd.",
|
||||
"MovieCannotBeFound": "Því miður er ekki hægt að finna þá kvikmynd.",
|
||||
"SslCertPassword": "SSL vottorð lykilorð",
|
||||
"SslCertPathHelpText": "Leið að pfx skrá",
|
||||
"SslPort": "SSL höfn",
|
||||
|
@ -602,7 +602,7 @@
|
|||
"GeneralSettingsSummary": "Gátt, SSL, notandanafn / lykilorð, umboð, greining og uppfærslur",
|
||||
"Genres": "Tegundir",
|
||||
"Global": "Alheimslegt",
|
||||
"GoToInterp": "Farðu í {0}",
|
||||
"MovieDetailsGoTo": "Farðu í {0}",
|
||||
"Grab": "Grípa",
|
||||
"Grabbed": "Greip",
|
||||
"GrabRelease": "Grípa losun",
|
||||
|
|
|
@ -542,7 +542,7 @@
|
|||
"HaveNotAddedMovies": "Non hai ancora aggiunto nessun film, vuoi prima importarne alcuni o tutti i tuoi film?",
|
||||
"Group": "Gruppo",
|
||||
"GrabReleaseMessageText": "{appName} non è stato in grado di determinare a quale film si riferisce questa release. {appName} potrebbe non essere in grado di importarla automaticamente. Vuoi catturare '{0}'?",
|
||||
"GoToInterp": "Vai a {0}",
|
||||
"MovieDetailsGoTo": "Vai a {0}",
|
||||
"Global": "Globale",
|
||||
"GeneralSettings": "Impostazioni Generali",
|
||||
"SupportedIndexersMoreInfo": "Per maggiori informazioni sui singoli Indexer clicca sul pulsante info.",
|
||||
|
@ -675,7 +675,7 @@
|
|||
"SslCertPassword": "Password Certificato SSL",
|
||||
"SourceRelativePath": "Percorso relativo origine",
|
||||
"SourcePath": "Percorso origine",
|
||||
"SorryThatMovieCannotBeFound": "Mi spiace, impossibile trovare il film.",
|
||||
"MovieCannotBeFound": "Mi spiace, impossibile trovare il film.",
|
||||
"SkipFreeSpaceCheck": "Salta controllo spazio libero",
|
||||
"ShowYear": "Mostra anno",
|
||||
"ShowUnknownMovieItems": "Mostra film sconosciuti",
|
||||
|
|
|
@ -550,7 +550,7 @@
|
|||
"GeneralSettings": "一般設定",
|
||||
"GeneralSettingsSummary": "ポート、SSL、ユーザー名/パスワード、プロキシ、分析、更新",
|
||||
"Global": "グローバル",
|
||||
"GoToInterp": "{0}に移動",
|
||||
"MovieDetailsGoTo": "{0}に移動",
|
||||
"Grab": "つかむ",
|
||||
"Grabbed": "掴んだ",
|
||||
"GrabRelease": "グラブリリース",
|
||||
|
@ -807,7 +807,7 @@
|
|||
"Socks4": "ソックス4",
|
||||
"Socks5": "Socks5(TORをサポート)",
|
||||
"SomeResultsHiddenFilter": "一部の結果は、適用されたフィルターによって非表示になります",
|
||||
"SorryThatMovieCannotBeFound": "申し訳ありませんが、その映画は見つかりません。",
|
||||
"MovieCannotBeFound": "申し訳ありませんが、その映画は見つかりません。",
|
||||
"Sort": "ソート",
|
||||
"Source": "ソース",
|
||||
"SourcePath": "ソースパス",
|
||||
|
|
|
@ -561,7 +561,7 @@
|
|||
"GeneralSettings": "일반 설정",
|
||||
"GeneralSettingsSummary": "포트, SSL, 사용자 이름 / 암호, 프록시, 분석 및 업데이트",
|
||||
"Global": "글로벌",
|
||||
"GoToInterp": "{0}로 이동",
|
||||
"MovieDetailsGoTo": "{0}로 이동",
|
||||
"Grab": "잡아",
|
||||
"Grabbed": "잡았다",
|
||||
"GrabRelease": "그랩 출시",
|
||||
|
@ -815,7 +815,7 @@
|
|||
"Small": "작게",
|
||||
"Socks4": "Socks4",
|
||||
"Socks5": "Socks5 (TOR 지원)",
|
||||
"SorryThatMovieCannotBeFound": "죄송합니다, 해당 영화를 찾을 수 없음",
|
||||
"MovieCannotBeFound": "죄송합니다, 해당 영화를 찾을 수 없음",
|
||||
"Sort": "정렬",
|
||||
"Source": "원본",
|
||||
"SourcePath": "원본 경로",
|
||||
|
|
|
@ -464,7 +464,7 @@
|
|||
"SetPermissionsLinuxHelpText": "Moet chmod worden uitgevoerd wanneer bestanden worden geïmporteerd/hernoemd?",
|
||||
"IconForCutoffUnmetHelpText": "Toon icoon voor bestanden waarbij de drempel niet behaald werd",
|
||||
"ShowUnknownMovieItems": "Toon onbekende film items",
|
||||
"SorryThatMovieCannotBeFound": "Sorry, deze film kan niet worden gevonden.",
|
||||
"MovieCannotBeFound": "Sorry, deze film kan niet worden gevonden.",
|
||||
"StandardMovieFormat": "Standaard Film Formaat",
|
||||
"TestAllIndexers": "Test Alle Indexeerders",
|
||||
"TorrentDelayHelpText": "Wachttijd in minuten alvorens een torrent op te halen",
|
||||
|
@ -617,7 +617,7 @@
|
|||
"ImportFailed": "Importeren mislukt: {sourceTitle}",
|
||||
"HiddenClickToShow": "Verborgen, klik om te tonen",
|
||||
"GrabReleaseMessageText": "{appName} was niet in staat om deze uitgave aan een film te koppelen. {appName} zal waarschijnlijk deze uitgave niet automatisch kunnen importeren. Wilt u '{0}' ophalen?",
|
||||
"GoToInterp": "Ga naar {0}",
|
||||
"MovieDetailsGoTo": "Ga naar {0}",
|
||||
"ExistingTag": "Bestaande tag",
|
||||
"ExcludeMovie": "Film Uitsluiten",
|
||||
"SearchIsNotSupportedWithThisIndexer": "Zoeken wordt niet ondersteund door deze indexeerder",
|
||||
|
|
|
@ -577,7 +577,7 @@
|
|||
"GeneralSettings": "Ustawienia główne",
|
||||
"GeneralSettingsSummary": "Port, SSL, nazwa użytkownika / hasło, proxy, analizy i aktualizacje",
|
||||
"Global": "Światowy",
|
||||
"GoToInterp": "Idź do {0}",
|
||||
"MovieDetailsGoTo": "Idź do {0}",
|
||||
"Grab": "Chwycić",
|
||||
"Grabbed": "Złapał",
|
||||
"GrabRelease": "Pobierz Wydanie",
|
||||
|
@ -822,7 +822,7 @@
|
|||
"Socks4": "Skarpetki 4",
|
||||
"Socks5": "Socks5 (Wsparcie TOR)",
|
||||
"SomeResultsHiddenFilter": "Niektóre wyniki są ukrywane przez zastosowany filtr",
|
||||
"SorryThatMovieCannotBeFound": "Przepraszamy, nie można znaleźć tego filmu.",
|
||||
"MovieCannotBeFound": "Przepraszamy, nie można znaleźć tego filmu.",
|
||||
"Sort": "Sortować",
|
||||
"Source": "Źródło",
|
||||
"SourcePath": "Ścieżka źródłowa",
|
||||
|
|
|
@ -561,7 +561,7 @@
|
|||
"HiddenClickToShow": "Oculto, clique para mostrar",
|
||||
"HaveNotAddedMovies": "Você ainda não adicionou nenhum filme. Quer importar alguns ou todos os seus filmes primeiro?",
|
||||
"GrabReleaseMessageText": "O {appName} não pode determinar a que filme pertence esta versão. O {appName} pode ser incapaz de importar automaticamente esta versão. Deseja capturar \"{0}\"?",
|
||||
"GoToInterp": "Ir para {0}",
|
||||
"MovieDetailsGoTo": "Ir para {0}",
|
||||
"SupportedListsMoreInfo": "Para obter mais informações sobre cada lista de importação, clique nos botões de informação.",
|
||||
"SupportedDownloadClientsMoreInfo": "Para obter mais informações sobre cada cliente de transferências, clique nos botões de mais informação.",
|
||||
"FilterPlaceHolder": "Pesquisar filmes",
|
||||
|
@ -620,7 +620,7 @@
|
|||
"SslCertPassword": "Palavra-passe do certificado SSL",
|
||||
"SourceRelativePath": "Caminho relativo de origem",
|
||||
"SourcePath": "Caminho de origem",
|
||||
"SorryThatMovieCannotBeFound": "Desculpe, este filme não foi encontrado.",
|
||||
"MovieCannotBeFound": "Desculpe, este filme não foi encontrado.",
|
||||
"SkipFreeSpaceCheck": "Pular verificação de espaço livre",
|
||||
"ShowYear": "Mostrar ano",
|
||||
"ShowUnknownMovieItems": "Mostrar itens desconhecidos do filme",
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
"GrabRelease": "Baixar lançamento",
|
||||
"Grabbed": "Obtido",
|
||||
"Grab": "Obter",
|
||||
"GoToInterp": "Ir para {0}",
|
||||
"MovieDetailsGoTo": "Ir para {0}",
|
||||
"Global": "Global",
|
||||
"Genres": "Gêneros",
|
||||
"GeneralSettingsSummary": "Porta, SSL, nome de usuário/senha, proxy, análises e atualizações",
|
||||
|
@ -619,7 +619,7 @@
|
|||
"SourcePath": "Caminho da Fonte",
|
||||
"Source": "Origem",
|
||||
"Sort": "Ordenar",
|
||||
"SorryThatMovieCannotBeFound": "Desculpe, esse filme não pode ser encontrado.",
|
||||
"MovieCannotBeFound": "Desculpe, esse filme não pode ser encontrado.",
|
||||
"SomeResultsHiddenFilter": "Alguns resultados estão ocultos pelo filtro aplicado",
|
||||
"Socks5": "Socks5 (Suporte à TOR)",
|
||||
"Small": "Pequeno",
|
||||
|
|
|
@ -316,7 +316,7 @@
|
|||
"ShowSearchHelpText": "Afișați butonul de căutare pe hover",
|
||||
"Socks5": "Șosete5 (Suport TOR)",
|
||||
"SomeResultsHiddenFilter": "Unele rezultate sunt ascunse de filtrul aplicat",
|
||||
"SorryThatMovieCannotBeFound": "Ne pare rău, filmul respectiv nu poate fi găsit.",
|
||||
"MovieCannotBeFound": "Ne pare rău, filmul respectiv nu poate fi găsit.",
|
||||
"TheLogLevelDefault": "Nivelul jurnalului este implicit la „Informații” și poate fi modificat în",
|
||||
"ThisCannotBeCancelled": "Acest lucru nu poate fi anulat odată pornit fără a reporni {appName}.",
|
||||
"TorrentDelayHelpText": "Întârziați în câteva minute pentru a aștepta înainte de a apuca un torent",
|
||||
|
@ -690,7 +690,7 @@
|
|||
"SupportedIndexersMoreInfo": "Pentru mai multe informații despre indexatorii individuali, faceți clic pe butoanele de informații.",
|
||||
"GeneralSettings": "Setări generale",
|
||||
"Global": "Global",
|
||||
"GoToInterp": "Accesați {0}",
|
||||
"MovieDetailsGoTo": "Accesați {0}",
|
||||
"Grab": "Apuca",
|
||||
"GrabRelease": "Grab Release",
|
||||
"GrabReleaseMessageText": "{appName} nu a putut stabili pentru ce film a fost lansată această lansare. Este posibil ca {appName} să nu poată importa automat această versiune. Doriți să luați „{0}”?",
|
||||
|
|
|
@ -575,7 +575,7 @@
|
|||
"GrabRelease": "Захватить релиз",
|
||||
"Grabbed": "Захвачено",
|
||||
"Grab": "Захватить",
|
||||
"GoToInterp": "Перейти {0}",
|
||||
"MovieDetailsGoTo": "Перейти {0}",
|
||||
"Global": "Глобальный",
|
||||
"Genres": "Жанры",
|
||||
"GeneralSettingsSummary": "Порт, SSL, имя пользователя/пароль, прокси, аналитика и обновления",
|
||||
|
@ -874,7 +874,7 @@
|
|||
"SourcePath": "Исходный путь",
|
||||
"Source": "Исходный код",
|
||||
"Sort": "Сортировка",
|
||||
"SorryThatMovieCannotBeFound": "Извините, этот фильм не найден.",
|
||||
"MovieCannotBeFound": "Извините, этот фильм не найден.",
|
||||
"SomeResultsHiddenFilter": "Некоторые результаты скрыты примененным фильтром",
|
||||
"Socks5": "Socks5 (Поддержка TOR)",
|
||||
"Socks4": "Socks4 прокси",
|
||||
|
|
|
@ -370,7 +370,7 @@
|
|||
"HiddenClickToShow": "Dold, klicka för att visa",
|
||||
"Group": "Grupp",
|
||||
"Folders": "Mappar",
|
||||
"GoToInterp": "Gå till {0}",
|
||||
"MovieDetailsGoTo": "Gå till {0}",
|
||||
"Global": "Global",
|
||||
"FilterPlaceHolder": "Sök filmer",
|
||||
"ExtraFileExtensionsHelpTextsExamples": "Exempel: '.sub, .nfo' eller 'sub,nfo'",
|
||||
|
@ -883,7 +883,7 @@
|
|||
"Small": "Små",
|
||||
"Socks4": "Strumpor4",
|
||||
"Socks5": "Socks5 (Support TOR)",
|
||||
"SorryThatMovieCannotBeFound": "Tyvärr kan den filmen inte hittas.",
|
||||
"MovieCannotBeFound": "Tyvärr kan den filmen inte hittas.",
|
||||
"SslCertPassword": "SSL-certifierat lösenord",
|
||||
"SslCertPasswordHelpText": "Lösenord för pfx-fil",
|
||||
"SslCertPath": "SSL-certifierad sökväg",
|
||||
|
|
|
@ -643,7 +643,7 @@
|
|||
"FreeSpace": "ที่ว่าง",
|
||||
"GeneralSettingsSummary": "พอร์ต SSL ชื่อผู้ใช้ / รหัสผ่านพร็อกซีการวิเคราะห์และอัปเดต",
|
||||
"Global": "ทั่วโลก",
|
||||
"GoToInterp": "ไปที่ {0}",
|
||||
"MovieDetailsGoTo": "ไปที่ {0}",
|
||||
"Grabbed": "คว้า",
|
||||
"GrabRelease": "คว้ารีลีส",
|
||||
"GrabSelected": "Grab Selected",
|
||||
|
@ -840,7 +840,7 @@
|
|||
"Socks4": "ถุงเท้า 4",
|
||||
"Socks5": "ถุงเท้า 5 (รองรับ TOR)",
|
||||
"SomeResultsHiddenFilter": "ผลลัพธ์บางอย่างถูกซ่อนไว้โดยตัวกรองที่ใช้",
|
||||
"SorryThatMovieCannotBeFound": "ขออภัยไม่พบภาพยนตร์เรื่องนั้น",
|
||||
"MovieCannotBeFound": "ขออภัยไม่พบภาพยนตร์เรื่องนั้น",
|
||||
"SourcePath": "เส้นทางแหล่งที่มา",
|
||||
"SourceRelativePath": "เส้นทางสัมพัทธ์ของแหล่งที่มา",
|
||||
"SourceTitle": "ชื่อแหล่งที่มา",
|
||||
|
|
|
@ -275,7 +275,7 @@
|
|||
"SkipFreeSpaceCheck": "Boş Alan Kontrolünü Atla",
|
||||
"Socks4": "Çorap4",
|
||||
"SomeResultsHiddenFilter": "Bazı sonuçlar, uygulanan filtre tarafından gizlendi",
|
||||
"SorryThatMovieCannotBeFound": "Maalesef o film bulunamıyor.",
|
||||
"MovieCannotBeFound": "Maalesef o film bulunamıyor.",
|
||||
"Source": "Kaynak",
|
||||
"SourcePath": "Kaynak Yolu",
|
||||
"SourceRelativePath": "Kaynak Göreceli Yol",
|
||||
|
@ -746,7 +746,7 @@
|
|||
"GeneralSettings": "Genel Ayarlar",
|
||||
"Genres": "Türler",
|
||||
"Global": "Küresel",
|
||||
"GoToInterp": "{0}'a git",
|
||||
"MovieDetailsGoTo": "{0}'a git",
|
||||
"Grabbed": "Alındı",
|
||||
"GrabRelease": "Sürüm Yakala",
|
||||
"Group": "Grup",
|
||||
|
|
|
@ -835,7 +835,7 @@
|
|||
"SizeLimit": "Обмеження розміру",
|
||||
"SizeOnDisk": "Розмір на диску",
|
||||
"Small": "Маленький",
|
||||
"SorryThatMovieCannotBeFound": "Вибачте, цей фільм неможливо знайти.",
|
||||
"MovieCannotBeFound": "Вибачте, цей фільм неможливо знайти.",
|
||||
"Sort": "Сортування",
|
||||
"Source": "Джерело",
|
||||
"SourceRelativePath": "Відносний шлях джерела",
|
||||
|
@ -977,7 +977,7 @@
|
|||
"DownloadClientStatusCheckSingleClientMessage": "Завантаження клієнтів недоступне через помилки: {downloadClientNames}",
|
||||
"Genres": "Жанри",
|
||||
"Global": "Глобальний",
|
||||
"GoToInterp": "Перейти до {0}",
|
||||
"MovieDetailsGoTo": "Перейти до {0}",
|
||||
"Group": "Група",
|
||||
"HardlinkCopyFiles": "Жорстке посилання/Копіювати файли",
|
||||
"HaveNotAddedMovies": "Ви ще не додали жодного фільму. Бажаєте спершу імпортувати деякі або всі свої фільми?",
|
||||
|
|
|
@ -531,7 +531,7 @@
|
|||
"ExcludeMovie": "Loại trừ Phim",
|
||||
"EnableSsl": "Bật SSL",
|
||||
"Path": "Con đường",
|
||||
"GoToInterp": "Đi tới {0}",
|
||||
"MovieDetailsGoTo": "Đi tới {0}",
|
||||
"Grab": "Vồ lấy",
|
||||
"Grabbed": "Nắm lấy",
|
||||
"ImportExtraFiles": "Nhập tệp bổ sung",
|
||||
|
@ -858,7 +858,7 @@
|
|||
"SizeOnDisk": "Kích thước trên đĩa",
|
||||
"SkipFreeSpaceCheck": "Bỏ qua kiểm tra dung lượng trống",
|
||||
"SomeResultsHiddenFilter": "Một số kết quả bị ẩn bởi bộ lọc được áp dụng",
|
||||
"SorryThatMovieCannotBeFound": "Xin lỗi, không thể tìm thấy bộ phim đó.",
|
||||
"MovieCannotBeFound": "Xin lỗi, không thể tìm thấy bộ phim đó.",
|
||||
"Sort": "Sắp xếp",
|
||||
"Source": "Nguồn",
|
||||
"SourcePath": "Đường dẫn nguồn",
|
||||
|
|
|
@ -495,7 +495,7 @@
|
|||
"SslCertPath": "SSL证书路径",
|
||||
"SourcePath": "来源路径",
|
||||
"Source": "代码",
|
||||
"SorryThatMovieCannotBeFound": "对不起,未找到该电影。",
|
||||
"MovieCannotBeFound": "对不起,未找到该电影。",
|
||||
"SkipFreeSpaceCheck": "跳过剩余空间检查",
|
||||
"SizeOnDisk": "磁盘占用大小",
|
||||
"Size": "大小",
|
||||
|
@ -608,7 +608,7 @@
|
|||
"HardlinkCopyFiles": "硬链接/复制文件",
|
||||
"GrabSelected": "抓取已选",
|
||||
"GrabRelease": "抓取版本",
|
||||
"GoToInterp": "跳转到 {0}",
|
||||
"MovieDetailsGoTo": "跳转到 {0}",
|
||||
"Genres": "类型",
|
||||
"MoveMovieFoldersRenameFolderWarning": "这也将根据设置中的电影文件夹格式重命名电影文件夹。",
|
||||
"FocusSearchBox": "聚焦搜索框",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue