mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
100 Commits
v5.18.2.96
...
v5.21.1.97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
886db23c58 | ||
|
|
b646386e77 | ||
|
|
4aa259a666 | ||
|
|
35f1a61bf8 | ||
|
|
1d855aed00 | ||
|
|
f7da5b0866 | ||
|
|
682cc70acf | ||
|
|
9d624b07ce | ||
|
|
2afb41498d | ||
|
|
a0679fcf11 | ||
|
|
df4a69ac02 | ||
|
|
2c8d8ff2d6 | ||
|
|
0593568065 | ||
|
|
25aa719ad6 | ||
|
|
3ab61a2fee | ||
|
|
954a040d6e | ||
|
|
905b23618a | ||
|
|
8decd5d8e1 | ||
|
|
8b5b177d16 | ||
|
|
e6c6fceff8 | ||
|
|
dcc8b28a07 | ||
|
|
02baf4d7a4 | ||
|
|
22ec1fe492 | ||
|
|
a7dbdadd21 | ||
|
|
93581e4a2f | ||
|
|
4c8da09df6 | ||
|
|
89666175a6 | ||
|
|
7a33e156a3 | ||
|
|
c7c07404b0 | ||
|
|
abeeee9363 | ||
|
|
23c30734d2 | ||
|
|
939e45e646 | ||
|
|
16ceba2392 | ||
|
|
94d620d878 | ||
|
|
ee0db93a0a | ||
|
|
f815b31c33 | ||
|
|
c078191b3d | ||
|
|
653b358fd3 | ||
|
|
6a7ed22b44 | ||
|
|
779292490a | ||
|
|
e4e96fc7f9 | ||
|
|
049bf7715e | ||
|
|
df4dfaac0b | ||
|
|
89c96b0a80 | ||
|
|
7db12b6e58 | ||
|
|
28dee7bc01 | ||
|
|
8ec60eb0a6 | ||
|
|
102849a697 | ||
|
|
95da7d7b47 | ||
|
|
22b5739967 | ||
|
|
cfba047d80 | ||
|
|
576d404e70 | ||
|
|
5959d4e51a | ||
|
|
2aca6c6e1d | ||
|
|
e8bbe0ee9f | ||
|
|
66332a110a | ||
|
|
36c66deb4b | ||
|
|
edec432244 | ||
|
|
554e15d438 | ||
|
|
553645a07c | ||
|
|
7de7e83c5b | ||
|
|
b7a46bedb0 | ||
|
|
0925769377 | ||
|
|
72244362fe | ||
|
|
c6526c34e9 | ||
|
|
efa2913dbc | ||
|
|
35c22a4ffa | ||
|
|
66d96e21da | ||
|
|
36d4e9e6cd | ||
|
|
7189d7b15c | ||
|
|
6e80113987 | ||
|
|
bb8a0dda63 | ||
|
|
525ed65687 | ||
|
|
3fbccc6af3 | ||
|
|
8e10eecfac | ||
|
|
a3b1512552 | ||
|
|
d375b5ffbe | ||
|
|
884abc0368 | ||
|
|
f8da7aae03 | ||
|
|
c165118d4d | ||
|
|
b3dd571a92 | ||
|
|
dd900eb739 | ||
|
|
66aae0c91c | ||
|
|
d888a0a2b3 | ||
|
|
cb5416a18c | ||
|
|
7977e0be05 | ||
|
|
cd836fef38 | ||
|
|
b0bfbe767c | ||
|
|
528b93dabe | ||
|
|
1edcbee5e1 | ||
|
|
8853dced9f | ||
|
|
c7aa1bae5e | ||
|
|
405ae77070 | ||
|
|
6236bc9b4f | ||
|
|
743c977e5b | ||
|
|
c0e5646f07 | ||
|
|
10094b4e66 | ||
|
|
d923406f08 | ||
|
|
69a9c72286 | ||
|
|
55b9477a01 |
@@ -6,7 +6,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"version": "20",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2024
|
||||
* Copyright 2010-2025
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.18.2'
|
||||
majorVersion: '5.21.1'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
15
docs.sh
15
docs.sh
@@ -1,13 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
RUNTIME="win-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
RUNTIME="linux-x64"
|
||||
RUNTIME="linux-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
RUNTIME="osx-x64"
|
||||
RUNTIME="osx-$ARCHITECTURE"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
}
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const { message } = data as DownloadFailedHistory;
|
||||
const { message, indexer } = data as DownloadFailedHistory;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
@@ -179,6 +179,10 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||
) : null}
|
||||
|
||||
{indexer ? (
|
||||
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||
) : null}
|
||||
|
||||
{message ? (
|
||||
<DescriptionListItem title={translate('Message')} data={message} />
|
||||
) : null}
|
||||
|
||||
@@ -37,7 +37,7 @@ interface HistoryDetailsModalProps {
|
||||
sourceTitle: string;
|
||||
data: HistoryData;
|
||||
downloadId?: string;
|
||||
isMarkingAsFailed: boolean;
|
||||
isMarkingAsFailed?: boolean;
|
||||
onMarkAsFailedPress: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
@@ -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,15 +1,19 @@
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import ExtraFilesAppState from './ExtraFilesAppState';
|
||||
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MovieBlocklistAppState from './MovieBlocklistAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieCreditAppState from './MovieCreditAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import PathsAppState from './PathsAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import ReleasesAppState from './ReleasesAppState';
|
||||
import RootFolderAppState from './RootFolderAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
@@ -50,6 +54,7 @@ export interface CustomFilter {
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
isSidebarVisible: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
@@ -64,16 +69,21 @@ interface AppState {
|
||||
blocklist: BlocklistAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
extraFiles: ExtraFilesAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieBlocklist: MovieBlocklistAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieCredits: MovieCreditAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
movieHistory: MovieHistoryAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
movies: MoviesAppState;
|
||||
organizePreview: OrganizePreviewAppState;
|
||||
parse: ParseAppState;
|
||||
paths: PathsAppState;
|
||||
queue: QueueAppState;
|
||||
releases: ReleasesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
|
||||
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;
|
||||
@@ -5,6 +5,8 @@ import AppSectionState, {
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
export type MovieHistoryAppState = AppSectionState<History>;
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History>,
|
||||
|
||||
6
frontend/src/App/State/MovieBlocklistAppState.ts
Normal file
6
frontend/src/App/State/MovieBlocklistAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
|
||||
type MovieBlocklistAppState = AppSectionState<Blocklist>;
|
||||
|
||||
export default MovieBlocklistAppState;
|
||||
@@ -1,7 +1,11 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
|
||||
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||
interface MovieCollectionAppState
|
||||
extends AppSectionState<MovieCollection>,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ interface MoviesAppState
|
||||
deleteOptions: {
|
||||
addImportExclusion: boolean;
|
||||
};
|
||||
|
||||
pendingChanges: Partial<Movie>;
|
||||
}
|
||||
|
||||
export default MoviesAppState;
|
||||
|
||||
13
frontend/src/App/State/OrganizePreviewAppState.ts
Normal file
13
frontend/src/App/State/OrganizePreviewAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
|
||||
export interface OrganizePreviewModel extends ModelBase {
|
||||
movieId: number;
|
||||
movieFileId: number;
|
||||
existingPath: string;
|
||||
newPath: string;
|
||||
}
|
||||
|
||||
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
|
||||
|
||||
export default OrganizePreviewAppState;
|
||||
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Release from 'typings/Release';
|
||||
|
||||
interface ReleasesAppState
|
||||
extends AppSectionState<Release>,
|
||||
AppSectionFilterState<Release> {}
|
||||
|
||||
export default ReleasesAppState;
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -172,7 +172,7 @@ class CollectionMovie extends Component {
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onEditMovieModalClose}
|
||||
|
||||
@@ -11,7 +11,7 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import MovieGenres from 'Movie/MovieGenres';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -212,7 +212,7 @@ class CollectionOverview extends Component {
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
<QualityProfileName
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import StackTrace from 'stacktrace-js';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ErrorBoundaryError.css';
|
||||
|
||||
interface ErrorBoundaryErrorProps {
|
||||
@@ -18,7 +19,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
className = styles.container,
|
||||
messageClassName = styles.message,
|
||||
detailsClassName = styles.details,
|
||||
message = 'There was an error loading this content',
|
||||
message = translate('ErrorLoadingContent'),
|
||||
error,
|
||||
info,
|
||||
} = props;
|
||||
|
||||
@@ -3,7 +3,10 @@ import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const statusTagList = [
|
||||
{ id: 'tba', name: 'TBA' },
|
||||
{ id: 'tba',
|
||||
get name() {
|
||||
return translate('Tba');
|
||||
} },
|
||||
{
|
||||
id: 'announced',
|
||||
get name() {
|
||||
|
||||
@@ -53,7 +53,7 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
<Button onPress={onAddCustomFilter}>
|
||||
Add Custom Filter
|
||||
{translate('AddCustomFilter')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||
}
|
||||
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
||||
// Listeners
|
||||
|
||||
onComputeMaxHeight = (data) => {
|
||||
const {
|
||||
top,
|
||||
bottom
|
||||
} = data.offsets.reference;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if ((/^botton/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom;
|
||||
} else {
|
||||
data.styles.maxHeight = top;
|
||||
}
|
||||
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -463,6 +456,10 @@ class EnhancedSelectInput extends Component {
|
||||
order: 851,
|
||||
enabled: true,
|
||||
fn: this.onComputeMaxHeight
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -18,15 +20,17 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = items.sort(sortByProp('name')).map((indexer) => ({
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
}));
|
||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function createMapStateToProps() {
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
value: `(${translate('Mixed')})`,
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ function createMapStateToProps() {
|
||||
|
||||
values.push({
|
||||
key: ADD_NEW_KEY,
|
||||
value: 'Add a new path'
|
||||
value: translate('AddANewPath')
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
|
||||
import styles from './RootFolderSelectInputOption.css';
|
||||
|
||||
@@ -47,14 +48,14 @@ function RootFolderSelectInputOption(props) {
|
||||
freeSpace == null ?
|
||||
null :
|
||||
<div className={styles.freeSpace}>
|
||||
{formatBytes(freeSpace)} Free
|
||||
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isMissing ?
|
||||
<div className={styles.isMissing}>
|
||||
Missing
|
||||
{translate('Missing')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
@@ -67,8 +68,8 @@ RootFolderSelectInputOption.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
movieFolder: PropTypes.string,
|
||||
isMissing: PropTypes.bool,
|
||||
movieFolder: PropTypes.string,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
||||
import styles from './RootFolderSelectInputSelectedValue.css';
|
||||
|
||||
@@ -39,7 +40,7 @@ function RootFolderSelectInputSelectedValue(props) {
|
||||
{
|
||||
freeSpace != null && includeFreeSpace &&
|
||||
<div className={styles.freeSpace}>
|
||||
{formatBytes(freeSpace)} Free
|
||||
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
|
||||
</div>
|
||||
}
|
||||
</EnhancedSelectInputSelectedValue>
|
||||
|
||||
@@ -8,27 +8,37 @@ import styles from './UMaskInput.css';
|
||||
const umaskOptions = [
|
||||
{
|
||||
key: '755',
|
||||
value: '755 - Owner write, Everyone else read',
|
||||
get value() {
|
||||
return translate('Umask755Description', { octal: '755' });
|
||||
},
|
||||
hint: 'drwxr-xr-x'
|
||||
},
|
||||
{
|
||||
key: '775',
|
||||
value: '775 - Owner & Group write, Other read',
|
||||
get value() {
|
||||
return translate('Umask775Description', { octal: '775' });
|
||||
},
|
||||
hint: 'drwxrwxr-x'
|
||||
},
|
||||
{
|
||||
key: '770',
|
||||
value: '770 - Owner & Group write',
|
||||
get value() {
|
||||
return translate('Umask770Description', { octal: '770' });
|
||||
},
|
||||
hint: 'drwxrwx---'
|
||||
},
|
||||
{
|
||||
key: '750',
|
||||
value: '750 - Owner write, Group read',
|
||||
get value() {
|
||||
return translate('Umask750Description', { octal: '750' });
|
||||
},
|
||||
hint: 'drwxr-x---'
|
||||
},
|
||||
{
|
||||
key: '777',
|
||||
value: '777 - Everyone write',
|
||||
get value() {
|
||||
return translate('Umask777Description', { octal: '777' });
|
||||
},
|
||||
hint: 'drwxrwxrwx'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -58,9 +58,9 @@ class FilterMenu extends Component {
|
||||
>
|
||||
<ButtonComponent
|
||||
iconName={icons.FILTER}
|
||||
showIndicator={selectedFilterKey !== 'all'}
|
||||
text={translate('Filter')}
|
||||
isDisabled={isDisabled}
|
||||
showIndicator={selectedFilterKey !== 'all'}
|
||||
/>
|
||||
|
||||
<FilterMenuContent
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Portal from 'Components/Portal';
|
||||
import { align } from 'Helpers/Props';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import getUniqueElementId from 'Utilities/getUniqueElementId';
|
||||
import styles from './Menu.css';
|
||||
|
||||
const sharedPopperOptions = {
|
||||
@@ -38,8 +38,8 @@ class Menu extends Component {
|
||||
super(props, context);
|
||||
|
||||
this._scheduleUpdate = null;
|
||||
this._menuButtonId = getUniqueElememtId();
|
||||
this._menuContentId = getUniqueElememtId();
|
||||
this._menuButtonId = getUniqueElementId();
|
||||
this._menuContentId = getUniqueElementId();
|
||||
|
||||
this.state = {
|
||||
isMenuOpen: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ function ModalError(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Error
|
||||
{translate('Error')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -26,7 +26,7 @@ function ModalError(props) {
|
||||
messageClassName={styles.message}
|
||||
detailsClassName={styles.details}
|
||||
{...otherProps}
|
||||
message={translate('ThereWasAnErrorLoadingThisItem')}
|
||||
message={translate('ErrorLoadingItem')}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ function KeyboardShortcutsModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Keyboard Shortcuts
|
||||
{translate('KeyboardShortcuts')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -92,7 +92,7 @@ class MovieSearchInput extends Component {
|
||||
if (item.type === ADD_NEW_TYPE) {
|
||||
return (
|
||||
<div className={styles.addNewMovieSuggestion}>
|
||||
Search for {query}
|
||||
{translate('SearchForQuery', { query })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class PageHeader extends Component {
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
aria-label="Donate"
|
||||
aria-label={translate('Donate')}
|
||||
to="https://radarr.video/donate"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
|
||||
@@ -10,7 +10,7 @@ function PageContentError(props) {
|
||||
<PageContentBody>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message={translate('ThereWasAnErrorLoadingThisPage')}
|
||||
message={translate('ErrorLoadingPage')}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
composes: link;
|
||||
|
||||
padding: 10px 24px;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.isActiveLink {
|
||||
@@ -41,10 +42,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.noIcon {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.status {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ interface CssExports {
|
||||
'isActiveParentLink': string;
|
||||
'item': string;
|
||||
'link': string;
|
||||
'noIcon': string;
|
||||
'status': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -63,9 +63,7 @@ class PageSidebarItem extends Component {
|
||||
</span>
|
||||
}
|
||||
|
||||
<span className={isChildItem ? styles.noIcon : null}>
|
||||
{typeof title === 'function' ? title() : title}
|
||||
</span>
|
||||
{typeof title === 'function' ? title() : title}
|
||||
|
||||
{
|
||||
!!StatusComponent &&
|
||||
|
||||
@@ -23,11 +23,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 3px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
color: var(--toolbarLabelColor);
|
||||
font-size: $extraSmallFontSize;
|
||||
line-height: calc($extraSmallFontSize + 1px);
|
||||
|
||||
@@ -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;
|
||||
50
frontend/src/Components/Page/Toolbar/PageToolbarButton.tsx
Normal file
50
frontend/src/Components/Page/Toolbar/PageToolbarButton.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
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}
|
||||
title={label}
|
||||
{...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;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
@@ -9,6 +10,7 @@ interface Column {
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
fixedSortDirection?: SortDirection;
|
||||
isVisible: boolean;
|
||||
isModifiable?: boolean;
|
||||
}
|
||||
|
||||
@@ -170,11 +170,11 @@ class TableOptionsModal extends Component {
|
||||
{
|
||||
canModifyColumns ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Columns')}</FormLabel>
|
||||
<FormLabel>{translate('TableColumns')}</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text={translate('TableOptionsColumnsMessage')}
|
||||
text={translate('TableColumnsHelpText')}
|
||||
/>
|
||||
|
||||
<div className={styles.columns}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TablePager.css';
|
||||
|
||||
class TablePager extends Component {
|
||||
@@ -156,7 +157,7 @@ class TablePager extends Component {
|
||||
|
||||
<div className={styles.recordsContainer}>
|
||||
<div className={styles.records}>
|
||||
Total records: {totalRecords}
|
||||
{translate('TotalRecords', { totalRecords })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,63 +7,63 @@ export const shortcuts = {
|
||||
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
|
||||
key: '?',
|
||||
get name() {
|
||||
return translate('OpenThisModal');
|
||||
return translate('KeyboardShortcutsOpenModal');
|
||||
}
|
||||
},
|
||||
|
||||
CLOSE_MODAL: {
|
||||
key: 'Esc',
|
||||
get name() {
|
||||
return translate('CloseCurrentModal');
|
||||
return translate('KeyboardShortcutsCloseModal');
|
||||
}
|
||||
},
|
||||
|
||||
ACCEPT_CONFIRM_MODAL: {
|
||||
key: 'Enter',
|
||||
get name() {
|
||||
return translate('AcceptConfirmationModal');
|
||||
return translate('KeyboardShortcutsConfirmModal');
|
||||
}
|
||||
},
|
||||
|
||||
MOVIE_SEARCH_INPUT: {
|
||||
key: 's',
|
||||
get name() {
|
||||
return translate('FocusSearchBox');
|
||||
return translate('KeyboardShortcutsFocusSearchBox');
|
||||
}
|
||||
},
|
||||
|
||||
SAVE_SETTINGS: {
|
||||
key: 'mod+s',
|
||||
get name() {
|
||||
return translate('SaveSettings');
|
||||
return translate('KeyboardShortcutsSaveSettings');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_TOP: {
|
||||
key: 'mod+home',
|
||||
get name() {
|
||||
return translate('MovieIndexScrollTop');
|
||||
return translate('KeyboardShortcutsMovieIndexScrollTop');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_BOTTOM: {
|
||||
key: 'mod+end',
|
||||
get name() {
|
||||
return translate('MovieIndexScrollBottom');
|
||||
return translate('KeyboardShortcutsMovieIndexScrollBottom');
|
||||
}
|
||||
},
|
||||
|
||||
DETAILS_NEXT: {
|
||||
key: '→',
|
||||
get name() {
|
||||
return translate('MovieDetailsNextMovie');
|
||||
return translate('KeyboardShortcutsMovieDetailsNextMovie');
|
||||
}
|
||||
},
|
||||
|
||||
DETAILS_PREVIOUS: {
|
||||
key: '←',
|
||||
get name() {
|
||||
return translate('MovieDetailsPreviousMovie');
|
||||
return translate('KeyboardShortcutsMovieDetailsPreviousMovie');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import translate from 'Utilities/String/translate';
|
||||
import * as filterTypes from './filterTypes';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
@@ -20,49 +21,127 @@ export const all = [
|
||||
|
||||
export const possibleFilterTypes = {
|
||||
[ARRAY]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' },
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain')
|
||||
}
|
||||
],
|
||||
|
||||
[CONTAINS]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' }
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
}
|
||||
],
|
||||
|
||||
[DATE]: [
|
||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||
{ key: filterTypes.IN_LAST, value: 'in the last' },
|
||||
{ key: filterTypes.NOT_IN_LAST, value: 'not in the last' },
|
||||
{ key: filterTypes.IN_NEXT, value: 'in the next' },
|
||||
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterIsBefore')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterIsAfter')
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_LAST,
|
||||
value: () => translate('FilterInLast')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_LAST,
|
||||
value: () => translate('FilterNotInLast')
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_NEXT,
|
||||
value: () => translate('FilterInNext')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_NEXT,
|
||||
value: () => translate('FilterNotInNext')
|
||||
}
|
||||
],
|
||||
|
||||
[EQUAL]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' }
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs')
|
||||
}
|
||||
],
|
||||
|
||||
[EXACT]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterIsNot')
|
||||
}
|
||||
],
|
||||
|
||||
[NUMBER]: [
|
||||
{ key: filterTypes.EQUAL, value: 'equal' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'greater than' },
|
||||
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'greater than or equal' },
|
||||
{ key: filterTypes.LESS_THAN, value: 'less than' },
|
||||
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'less than or equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterGreaterThan')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterGreaterThanOrEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterLessThan')
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterLessThanOrEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual')
|
||||
}
|
||||
],
|
||||
|
||||
[STRING]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' },
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
|
||||
{ key: filterTypes.EQUAL, value: 'equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'not equal' },
|
||||
{ key: filterTypes.STARTS_WITH, value: 'starts with' },
|
||||
{ key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' },
|
||||
{ key: filterTypes.ENDS_WITH, value: 'ends with' },
|
||||
{ key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' }
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain')
|
||||
},
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.STARTS_WITH,
|
||||
value: () => translate('FilterStartsWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_STARTS_WITH,
|
||||
value: () => translate('FilterDoesNotStartWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.ENDS_WITH,
|
||||
value: () => translate('FilterEndsWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_ENDS_WITH,
|
||||
value: () => translate('FilterDoesNotEndWith')
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
reprocessInteractiveImportItems,
|
||||
updateInteractiveImportItem,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import Rejection from 'typings/Rejection';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -52,7 +53,7 @@ interface InteractiveImportRowProps {
|
||||
quality?: QualityModel;
|
||||
languages?: Language[];
|
||||
size: number;
|
||||
customFormats?: object[];
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore?: number;
|
||||
indexerFlags: number;
|
||||
rejections: Rejection[];
|
||||
|
||||
@@ -2,6 +2,7 @@ import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import Movie from 'Movie/Movie';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import Rejection from 'typings/Rejection';
|
||||
|
||||
export interface InteractiveImportCommandOptions {
|
||||
@@ -27,7 +28,7 @@ interface InteractiveImport extends ModelBase {
|
||||
languages: Language[];
|
||||
movie?: Movie;
|
||||
qualityWeight: number;
|
||||
customFormats: object[];
|
||||
customFormats: CustomFormat[];
|
||||
indexerFlags: number;
|
||||
rejections: Rejection[];
|
||||
movieFileId?: number;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -65,7 +65,7 @@ interface RowItemData {
|
||||
}
|
||||
|
||||
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
const { items, columns, onMovieSelect } = data;
|
||||
const { items, onMovieSelect } = data;
|
||||
const movie = index >= items.length ? null : items[index];
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
@@ -88,13 +88,11 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
onPress={handlePress}
|
||||
>
|
||||
<SelectMovieRow
|
||||
id={movie.id}
|
||||
key={movie.id}
|
||||
title={movie.title}
|
||||
tmdbId={movie.tmdbId}
|
||||
imdbId={movie.imdbId}
|
||||
year={movie.year}
|
||||
columns={columns}
|
||||
onMovieSelect={onMovieSelect}
|
||||
/>
|
||||
</VirtualTableRowButton>
|
||||
);
|
||||
@@ -181,7 +179,9 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{modalTitle} - Select Movie</ModalHeader>
|
||||
<ModalHeader>
|
||||
{translate('SelectMovieModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
@@ -189,7 +189,7 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder="Filter movies"
|
||||
placeholder={translate('FilterMoviePlaceholder')}
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
@@ -225,7 +225,7 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>Cancel</Button>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import styles from './SelectMovieRow.css';
|
||||
|
||||
class SelectMovieRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onMovieSelect(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<VirtualTableRowCell className={styles.title}>
|
||||
{this.props.title}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.year}>
|
||||
{this.props.year}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.imdbId}>
|
||||
{
|
||||
this.props.imdbId ?
|
||||
<Label>{this.props.imdbId}</Label> :
|
||||
null
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.tmdbId}>
|
||||
<Label>{this.props.tmdbId}</Label>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectMovieRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
imdbId: PropTypes.string,
|
||||
year: PropTypes.number.isRequired,
|
||||
onMovieSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectMovieRow;
|
||||
33
frontend/src/InteractiveImport/Movie/SelectMovieRow.tsx
Normal file
33
frontend/src/InteractiveImport/Movie/SelectMovieRow.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import styles from './SelectMovieRow.css';
|
||||
|
||||
interface SelectMovieRowProps {
|
||||
title: string;
|
||||
tmdbId: number;
|
||||
imdbId?: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
function SelectMovieRow({ title, year, tmdbId, imdbId }: SelectMovieRowProps) {
|
||||
return (
|
||||
<>
|
||||
<VirtualTableRowCell className={styles.title}>
|
||||
{title}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.year}>{year}</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.imdbId}>
|
||||
{imdbId ? <Label>{imdbId}</Label> : null}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.tmdbId}>
|
||||
<Label>{tmdbId}</Label>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectMovieRow;
|
||||
@@ -1,240 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
label: () => translate('History'),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections')
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function InteractiveSearch(props) {
|
||||
const {
|
||||
searchPayload,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalReleasesCount,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onSortPress,
|
||||
onFilterSelect,
|
||||
onGrabPress
|
||||
} = props;
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const type = 'movies';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
filterModalConnectorComponentProps={{ type }}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||
{
|
||||
errorMessage ?
|
||||
<Fragment>
|
||||
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
|
||||
</Fragment> :
|
||||
translate('MovieSearchResultsLoadError')
|
||||
}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !totalReleasesCount ?
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('NoResultsFound')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!totalReleasesCount && isPopulated && !items.length ?
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('AllResultsHiddenFilter')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRowConnector
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onGrabPress={onGrabPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
totalReleasesCount !== items.length && !!items.length ?
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('SomeResultsHiddenFilter')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearch.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalReleasesCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearch;
|
||||
263
frontend/src/InteractiveSearch/InteractiveSearch.tsx
Normal file
263
frontend/src/InteractiveSearch/InteractiveSearch.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import ReleasesAppState from 'App/State/ReleasesAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import {
|
||||
fetchReleases,
|
||||
grabRelease,
|
||||
setReleasesFilter,
|
||||
setReleasesSort,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModal from './InteractiveSearchFilterModal';
|
||||
import InteractiveSearchPayload from './InteractiveSearchPayload';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
label: () => translate('History'),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections'),
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
interface InteractiveSearchProps {
|
||||
searchPayload: InteractiveSearchPayload;
|
||||
}
|
||||
|
||||
function InteractiveSearch({ searchPayload }: InteractiveSearchProps) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
totalItems,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
}: ReleasesAppState & ClientSideCollectionAppState = useSelector(
|
||||
createClientSideCollectionSelector('releases')
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string | number) => {
|
||||
dispatch(setReleasesFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string, sortDirection?: SortDirection) => {
|
||||
dispatch(setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleGrabPress = useCallback(
|
||||
(payload: object) => {
|
||||
dispatch(grabRelease(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Only fetch releases if they are not already being fetched and not yet populated.
|
||||
|
||||
if (!isFetching && !isPopulated) {
|
||||
dispatch(fetchReleases(searchPayload));
|
||||
|
||||
const { movieId } = searchPayload;
|
||||
|
||||
if (movieId) {
|
||||
dispatch(fetchMovieBlocklist({ movieId }));
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModal}
|
||||
filterModalConnectorComponentProps={{ type: 'movies' }}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||
{errorMessage ? (
|
||||
<>
|
||||
{translate('InteractiveSearchResultsFailedErrorMessage', {
|
||||
message:
|
||||
errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1),
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
translate('MovieSearchResultsLoadError')
|
||||
)}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && isPopulated && !totalItems ? (
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('NoResultsFound')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!!totalItems && isPopulated && !items.length ? (
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('AllResultsHiddenFilter')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
onGrabPress={handleGrabPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{totalItems !== items.length && !!items.length ? (
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('SomeResultsHiddenFilter')}
|
||||
</Alert>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveSearch;
|
||||
@@ -1,109 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import InteractiveSearch from './InteractiveSearch';
|
||||
|
||||
function createMapStateToProps(appState) {
|
||||
return createSelector(
|
||||
(state) => state.releases.items.length,
|
||||
createClientSideCollectionSelector('releases'),
|
||||
createUISettingsSelector(),
|
||||
(totalReleasesCount, releases, uiSettings) => {
|
||||
return {
|
||||
totalReleasesCount,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
...releases
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchReleases(payload) {
|
||||
dispatch(releaseActions.fetchReleases(payload));
|
||||
},
|
||||
|
||||
dispatchFetchMovieHistory({ movieId }) {
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
},
|
||||
|
||||
dispatchClearMovieHistory() {
|
||||
dispatch(clearMovieHistory());
|
||||
},
|
||||
|
||||
onSortPress(sortKey, sortDirection) {
|
||||
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
onGrabPress(payload) {
|
||||
dispatch(releaseActions.grabRelease(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class InteractiveSearchConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
searchPayload,
|
||||
isPopulated,
|
||||
dispatchFetchReleases,
|
||||
dispatchFetchMovieHistory
|
||||
} = this.props;
|
||||
|
||||
// If search results are not yet isPopulated fetch them,
|
||||
// otherwise re-show the existing props.
|
||||
if (!isPopulated) {
|
||||
dispatchFetchReleases(searchPayload);
|
||||
}
|
||||
|
||||
dispatchFetchMovieHistory(searchPayload);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearMovieHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchReleases,
|
||||
dispatchFetchMovieHistory,
|
||||
dispatchClearMovieHistory,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
||||
<InteractiveSearch
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchConnector.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieHistory: PropTypes.func.isRequired,
|
||||
dispatchClearMovieHistory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
|
||||
function createReleasesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.releases.items,
|
||||
(releases) => {
|
||||
return releases;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.releases.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface InteractiveSearchFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function InteractiveSearchFilterModal({
|
||||
...otherProps
|
||||
}: InteractiveSearchFilterModalProps) {
|
||||
const sectionItems = useSelector(createReleasesSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setReleasesFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...otherProps}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType="releases"
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.releases.items,
|
||||
(state) => state.releases.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'releases'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchSetFilter(payload) {
|
||||
const action = setReleasesFilter;
|
||||
dispatch(action(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
||||
@@ -0,0 +1,7 @@
|
||||
interface MovieSearchPayload {
|
||||
movieId: number;
|
||||
}
|
||||
|
||||
type InteractiveSearchPayload = MovieSearchPayload;
|
||||
|
||||
export default InteractiveSearchPayload;
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
@@ -8,21 +11,18 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import MovieBlocklist from 'typings/MovieBlocklist';
|
||||
import MovieHistory from 'typings/MovieHistory';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import Release from 'typings/Release';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchPayload from './InteractiveSearchPayload';
|
||||
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
@@ -71,37 +71,42 @@ function getDownloadTooltip(
|
||||
return translate('AddToDownloadQueue');
|
||||
}
|
||||
|
||||
interface InteractiveSearchRowProps {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
infoUrl: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
size: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mappedMovieId?: number;
|
||||
indexerFlags: string[];
|
||||
rejections: string[];
|
||||
downloadAllowed: boolean;
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
historyFailedData?: MovieHistory;
|
||||
historyGrabbedData?: MovieHistory;
|
||||
blocklistData?: MovieBlocklist;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
searchPayload: object;
|
||||
function releaseHistorySelector({ guid }: Release) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movieHistory.items,
|
||||
(state: AppState) => state.movieBlocklist.items,
|
||||
(movieHistory, movieBlocklist) => {
|
||||
let historyFailedData = null;
|
||||
let blocklistedData = null;
|
||||
|
||||
const historyGrabbedData = movieHistory.find(
|
||||
({ eventType, data }) =>
|
||||
eventType === 'grabbed' && 'guid' in data && data.guid === guid
|
||||
);
|
||||
|
||||
if (historyGrabbedData) {
|
||||
historyFailedData = movieHistory.find(
|
||||
({ eventType, sourceTitle }) =>
|
||||
eventType === 'downloadFailed' &&
|
||||
sourceTitle === historyGrabbedData.sourceTitle
|
||||
);
|
||||
|
||||
blocklistedData = movieBlocklist.find(
|
||||
(item) => item.sourceTitle === historyGrabbedData.sourceTitle
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistedData,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface InteractiveSearchRowProps extends Release {
|
||||
searchPayload: InteractiveSearchPayload;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
@@ -130,16 +135,18 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
downloadAllowed,
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
historyGrabbedData = {} as MovieHistory,
|
||||
historyFailedData = {} as MovieHistory,
|
||||
blocklistData = {} as MovieBlocklist,
|
||||
searchPayload,
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const { longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
const { historyGrabbedData, historyFailedData, blocklistedData } =
|
||||
useSelector(releaseHistorySelector(props));
|
||||
|
||||
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
@@ -211,44 +218,52 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{historyGrabbedData?.date && !historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DEFAULT}
|
||||
title={`${translate('Grabbed')}: ${formatDateTime(
|
||||
historyGrabbedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DEFAULT} />}
|
||||
tooltip={translate('GrabbedAt', {
|
||||
date: formatDateTime(
|
||||
historyGrabbedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Failed')}: ${formatDateTime(
|
||||
historyFailedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DANGER} />}
|
||||
tooltip={translate('FailedAt', {
|
||||
date: formatDateTime(
|
||||
historyFailedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{blocklistData?.date ? (
|
||||
{blocklistedData?.date ? (
|
||||
<Icon
|
||||
className={
|
||||
historyGrabbedData || historyFailedData ? styles.blocklist : ''
|
||||
}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Blocklisted')}: ${formatDateTime(
|
||||
blocklistData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
title={translate('BlocklistedAt', {
|
||||
date: formatDateTime(
|
||||
blocklistedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { guid }) => guid,
|
||||
(state) => state.movieHistory.items,
|
||||
(state) => state.movieBlocklist.items,
|
||||
(guid, movieHistory, movieBlocklist) => {
|
||||
|
||||
let blocklistData = {};
|
||||
let historyFailedData = {};
|
||||
|
||||
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
|
||||
if (historyGrabbedData) {
|
||||
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
blocklistData = movieBlocklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
}
|
||||
|
||||
return {
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class InteractiveSearchRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
historyGrabbedData={historyGrabbedData}
|
||||
historyFailedData={historyFailedData}
|
||||
blocklistData={blocklistData}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchRowConnector.propTypes = {
|
||||
historyGrabbedData: PropTypes.object,
|
||||
historyFailedData: PropTypes.object,
|
||||
blocklistData: PropTypes.object
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(InteractiveSearchRowConnector);
|
||||
@@ -1,9 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function getKind(seeders) {
|
||||
function getKind(seeders: number = 0) {
|
||||
if (seeders > 50) {
|
||||
return kinds.PRIMARY;
|
||||
}
|
||||
@@ -19,7 +18,7 @@ function getKind(seeders) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
function getPeersTooltipPart(peers, peersUnit) {
|
||||
function getPeersTooltipPart(peers: number | undefined, peersUnit: string) {
|
||||
if (peers == null) {
|
||||
return `Unknown ${peersUnit}s`;
|
||||
}
|
||||
@@ -31,27 +30,27 @@ function getPeersTooltipPart(peers, peersUnit) {
|
||||
return `${peers} ${peersUnit}s`;
|
||||
}
|
||||
|
||||
function Peers(props) {
|
||||
const {
|
||||
seeders,
|
||||
leechers
|
||||
} = props;
|
||||
interface PeersProps {
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
}
|
||||
|
||||
function Peers(props: PeersProps) {
|
||||
const { seeders, leechers } = props;
|
||||
|
||||
const kind = getKind(seeders);
|
||||
|
||||
return (
|
||||
<Label
|
||||
kind={kind}
|
||||
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(leechers, 'leecher')}`}
|
||||
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(
|
||||
leechers,
|
||||
'leecher'
|
||||
)}`}
|
||||
>
|
||||
{seeders == null ? '-' : seeders} / {leechers == null ? '-' : leechers}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
Peers.propTypes = {
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number
|
||||
};
|
||||
|
||||
export default Peers;
|
||||
@@ -1,37 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DeleteMovieModalContentConnector from './DeleteMovieModalContentConnector';
|
||||
|
||||
function DeleteMovieModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
previousMovie,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<DeleteMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
previousMovie={previousMovie}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteMovieModal.propTypes = {
|
||||
...DeleteMovieModalContentConnector.propTypes,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
previousMovie: PropTypes.string
|
||||
};
|
||||
|
||||
export default DeleteMovieModal;
|
||||
24
frontend/src/Movie/Delete/DeleteMovieModal.tsx
Normal file
24
frontend/src/Movie/Delete/DeleteMovieModal.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DeleteMovieModalContent, {
|
||||
DeleteMovieModalContentProps,
|
||||
} from './DeleteMovieModalContent';
|
||||
|
||||
interface DeleteMovieModalProps extends DeleteMovieModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function DeleteMovieModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: DeleteMovieModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||
<DeleteMovieModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteMovieModal;
|
||||
@@ -1,163 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteMovieModalContent.css';
|
||||
|
||||
class DeleteMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
deleteFiles: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeleteFilesChange = ({ value }) => {
|
||||
this.setState({ deleteFiles: value });
|
||||
};
|
||||
|
||||
onDeleteMovieConfirmed = () => {
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = this.props.deleteOptions.addImportExclusion;
|
||||
|
||||
this.setState({ deleteFiles: false });
|
||||
this.props.onDeletePress(deleteFiles, addImportExclusion);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
path,
|
||||
statistics = {},
|
||||
deleteOptions,
|
||||
onModalClose,
|
||||
onDeleteOptionChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
movieFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = deleteOptions.addImportExclusion;
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('DeleteHeader', { title })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.pathContainer}>
|
||||
<Icon
|
||||
className={styles.pathIcon}
|
||||
name={icons.FOLDER}
|
||||
/>
|
||||
|
||||
{path}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('AddListExclusion')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportExclusion"
|
||||
value={addImportExclusion}
|
||||
helpText={translate('AddListExclusionMovieHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={onDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{movieFileCount === 0 ? translate('DeleteMovieFolder') : translate('DeleteMovieFiles', { movieFileCount })}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={movieFileCount === 0 ? translate('DeleteMovieFolderHelpText') : translate('DeleteMovieFilesHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={this.onDeleteFilesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
deleteFiles ?
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div><InlineMarkdown data={translate('DeleteMovieFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
|
||||
|
||||
{
|
||||
movieFileCount ?
|
||||
<div className={styles.deleteCount}>
|
||||
{translate('DeleteMovieFolderMovieCount', { movieFileCount, size: formatBytes(sizeOnDisk) })}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteMovieConfirmed}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteMovieModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
deleteOptions: PropTypes.object.isRequired,
|
||||
onDeleteOptionChange: PropTypes.func.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DeleteMovieModalContent.defaultProps = {
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default DeleteMovieModalContent;
|
||||
158
frontend/src/Movie/Delete/DeleteMovieModalContent.tsx
Normal file
158
frontend/src/Movie/Delete/DeleteMovieModalContent.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteMovieModalContent.css';
|
||||
|
||||
export interface DeleteMovieModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function DeleteMovieModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
}: DeleteMovieModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
title,
|
||||
path,
|
||||
collection,
|
||||
statistics = {} as Statistics,
|
||||
} = useMovie(movieId)!;
|
||||
const { addImportExclusion } = useSelector(
|
||||
(state: AppState) => state.movies.deleteOptions
|
||||
);
|
||||
|
||||
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const handleDeleteFilesChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setDeleteFiles(value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeleteMovieConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
deleteMovie({
|
||||
id: movieId,
|
||||
collectionTmdbId: collection?.tmdbId,
|
||||
deleteFiles,
|
||||
addImportExclusion,
|
||||
})
|
||||
);
|
||||
|
||||
onModalClose();
|
||||
}, [
|
||||
movieId,
|
||||
collection,
|
||||
addImportExclusion,
|
||||
deleteFiles,
|
||||
dispatch,
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
const handleDeleteOptionChange = useCallback(
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
dispatch(setDeleteOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('DeleteHeader', { title })}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.pathContainer}>
|
||||
<Icon className={styles.pathIcon} name={icons.FOLDER} />
|
||||
|
||||
{path}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AddListExclusion')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportExclusion"
|
||||
value={addImportExclusion}
|
||||
helpText={translate('AddListExclusionMovieHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={handleDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{movieFileCount === 0
|
||||
? translate('DeleteMovieFolder')
|
||||
: translate('DeleteMovieFiles', { movieFileCount })}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={
|
||||
movieFileCount === 0
|
||||
? translate('DeleteMovieFolderHelpText')
|
||||
: translate('DeleteMovieFilesHelpText')
|
||||
}
|
||||
kind={kinds.DANGER}
|
||||
onChange={handleDeleteFilesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{deleteFiles ? (
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('DeleteMovieFolderConfirmation', { path })}
|
||||
blockClassName={styles.folderPath}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{movieFileCount ? (
|
||||
<div className={styles.deleteCount}>
|
||||
{translate('DeleteMovieFolderMovieCount', {
|
||||
movieFileCount,
|
||||
size: formatBytes(sizeOnDisk),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleDeleteMovieConfirmed}>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteMovieModalContent;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import DeleteMovieModalContent from './DeleteMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies.deleteOptions,
|
||||
createMovieSelector(),
|
||||
(deleteOptions, movie) => {
|
||||
return {
|
||||
...movie,
|
||||
deleteOptions
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onDeleteOptionChange(option) {
|
||||
dispatch(
|
||||
setDeleteOption({
|
||||
[option.name]: option.value
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
onDeletePress(deleteFiles, addImportExclusion) {
|
||||
dispatch(
|
||||
deleteMovie({
|
||||
id: props.movieId,
|
||||
collectionTmdbId: this.collection?.tmdbId,
|
||||
deleteFiles,
|
||||
addImportExclusion
|
||||
})
|
||||
);
|
||||
|
||||
props.onModalClose(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteMovieModalContent);
|
||||
@@ -160,10 +160,10 @@
|
||||
}
|
||||
|
||||
.overview {
|
||||
flex: 1 0 auto;
|
||||
flex: 1 0 0;
|
||||
margin-top: 8px;
|
||||
padding-left: 7px;
|
||||
min-height: 0;
|
||||
text-wrap: balance;
|
||||
font-size: $intermediateFontSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,832 +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, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||
import 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 OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
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}>
|
||||
<MovieCollectionLabelConnector
|
||||
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>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
isOpen={isOrganizeModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
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}
|
||||
nextMovieRelativePath={`/movie/${nextMovie.titleSlug}`}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
movieId={id}
|
||||
modalTitle={translate('ManageFiles')}
|
||||
folder={path}
|
||||
allowMovieChange={false}
|
||||
showFilterExistingFiles={true}
|
||||
showImportMode={false}
|
||||
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;
|
||||
981
frontend/src/Movie/Details/MovieDetails.tsx
Normal file
981
frontend/src/Movie/Details/MovieDetails.tsx
Normal file
@@ -0,0 +1,981 @@
|
||||
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 useMovie from 'Movie/useMovie';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
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 {
|
||||
cancelFetchReleases,
|
||||
clearReleases,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
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,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface MovieDetailsProps {
|
||||
movieId: number;
|
||||
}
|
||||
|
||||
function MovieDetails({ movieId }: MovieDetailsProps) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const movie = useMovie(movieId);
|
||||
const allMovies = useSelector(createAllMoviesSelector());
|
||||
|
||||
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 { 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 { nextMovie, previousMovie } = useMemo(() => {
|
||||
const sortedMovies = [...allMovies].sort(sortByProp('sortTitle'));
|
||||
const movieIndex = sortedMovies.findIndex((movie) => movie.id === movieId);
|
||||
|
||||
if (movieIndex === -1) {
|
||||
return {
|
||||
nextMovie: undefined,
|
||||
previousMovie: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const nextMovie = sortedMovies[movieIndex + 1] ?? sortedMovies[0];
|
||||
const previousMovie =
|
||||
sortedMovies[movieIndex - 1] ?? sortedMovies[sortedMovies.length - 1];
|
||||
|
||||
return {
|
||||
nextMovie: {
|
||||
title: nextMovie.title,
|
||||
titleSlug: nextMovie.titleSlug,
|
||||
},
|
||||
previousMovie: {
|
||||
title: previousMovie.title,
|
||||
titleSlug: previousMovie.titleSlug,
|
||||
},
|
||||
};
|
||||
}, [movieId, allMovies]);
|
||||
|
||||
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 (
|
||||
isOrganizeModalOpen ||
|
||||
isManageMoviesModalOpen ||
|
||||
isInteractiveSearchModalOpen ||
|
||||
isEditMovieModalOpen ||
|
||||
isDeleteMovieModalOpen ||
|
||||
isMovieHistoryModalOpen
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
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}`));
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
isOrganizeModalOpen,
|
||||
isManageMoviesModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
isEditMovieModalOpen,
|
||||
isDeleteMovieModalOpen,
|
||||
isMovieHistoryModalOpen,
|
||||
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());
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
};
|
||||
}, [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,
|
||||
isSaving = false,
|
||||
} = 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}>
|
||||
{previousMovie ? (
|
||||
<IconButton
|
||||
className={styles.movieNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={translate('MovieDetailsGoTo', {
|
||||
title: previousMovie.title,
|
||||
})}
|
||||
to={`/movie/${previousMovie.titleSlug}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{nextMovie ? (
|
||||
<IconButton
|
||||
className={styles.movieNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={translate('MovieDetailsGoTo', {
|
||||
title: nextMovie.title,
|
||||
})}
|
||||
to={`/movie/${nextMovie.titleSlug}`}
|
||||
/>
|
||||
) : null}
|
||||
</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}>
|
||||
<QualityProfileName 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,356 +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 { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
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());
|
||||
},
|
||||
dispatchClearReleases() {
|
||||
dispatch(clearReleases());
|
||||
},
|
||||
dispatchCancelFetchReleases() {
|
||||
dispatch(cancelFetchReleases());
|
||||
},
|
||||
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}`));
|
||||
},
|
||||
dispatchFetchMovieBlocklist({ movieId }) {
|
||||
dispatch(fetchMovieBlocklist({ movieId }));
|
||||
},
|
||||
dispatchClearMovieBlocklist() {
|
||||
dispatch(clearMovieBlocklist());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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.dispatchFetchMovieBlocklist({ movieId });
|
||||
this.props.dispatchFetchExtraFiles({ movieId });
|
||||
this.props.dispatchFetchMovieCredits({ movieId });
|
||||
this.props.dispatchFetchQueueDetails({ movieId });
|
||||
this.props.dispatchFetchImportListSchema();
|
||||
};
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearMovieBlocklist();
|
||||
this.props.dispatchClearMovieFiles();
|
||||
this.props.dispatchClearExtraFiles();
|
||||
this.props.dispatchClearMovieCredits();
|
||||
this.props.dispatchClearQueueDetails();
|
||||
this.props.dispatchClearReleases();
|
||||
};
|
||||
|
||||
//
|
||||
// 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,
|
||||
dispatchClearReleases: PropTypes.func.isRequired,
|
||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchToggleMovieMonitored: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchClearQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchImportListSchema: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieBlocklist: PropTypes.func.isRequired,
|
||||
dispatchClearMovieBlocklist: PropTypes.func.isRequired,
|
||||
onGoToMovie: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieDetailsConnector);
|
||||
@@ -92,6 +92,19 @@ function MovieDetailsLinks(props: MovieDetailsLinksProps) {
|
||||
MDBList
|
||||
</Label>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://www.blu-ray.com/search/?quicksearch=1&quicksearch_keyword=${imdbId}§ion=theatrical`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Blu-ray
|
||||
</Label>
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
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,26 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditMovieModalContentConnector from './EditMovieModalContentConnector';
|
||||
|
||||
function EditMovieModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditMovieModal.propTypes = {
|
||||
...EditMovieModalContentConnector.propTypes,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditMovieModal;
|
||||
32
frontend/src/Movie/Edit/EditMovieModal.tsx
Normal file
32
frontend/src/Movie/Edit/EditMovieModal.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditMovieModalContent, {
|
||||
EditMovieModalContentProps,
|
||||
} from './EditMovieModalContent';
|
||||
|
||||
interface EditMovieModalProps extends EditMovieModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditMovieModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditMovieModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'movies' }));
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditMovieModalContent {...otherProps} onModalClose={handleModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditMovieModal;
|
||||
@@ -1,40 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditMovieModal from './EditMovieModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditMovieModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'movies' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditMovieModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditMovieModalConnector.propTypes = {
|
||||
...EditMovieModal.propTypes,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(EditMovieModalConnector);
|
||||
@@ -1,217 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
|
||||
class EditMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmMoveModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCancelPress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
isPathChanging,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
|
||||
this.setState({ isConfirmMoveModalOpen: true });
|
||||
} else {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
onSavePress(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMoveMoviePress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
this.props.onSavePress(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
item,
|
||||
isSaving,
|
||||
originalPath,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
onDeleteMoviePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
// Id,
|
||||
path,
|
||||
tags
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('Edit')} - {title}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredHelpText')}
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Path')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="path"
|
||||
{...path}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteMoviePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveMovieModal
|
||||
originalPath={originalPath}
|
||||
destinationPath={path.value}
|
||||
isOpen={this.state.isConfirmMoveModalOpen}
|
||||
onModalClose={this.onCancelPress}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditMovieModalContent.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isPathChanging: PropTypes.bool.isRequired,
|
||||
originalPath: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditMovieModalContent;
|
||||
235
frontend/src/Movie/Edit/EditMovieModalContent.tsx
Normal file
235
frontend/src/Movie/Edit/EditMovieModalContent.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
icons,
|
||||
inputTypes,
|
||||
kinds,
|
||||
sizes,
|
||||
tooltipPositions,
|
||||
} from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
|
||||
export interface EditMovieModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteMoviePress: () => void;
|
||||
}
|
||||
|
||||
function EditMovieModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
onDeleteMoviePress,
|
||||
}: EditMovieModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
title,
|
||||
monitored,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
path,
|
||||
tags,
|
||||
} = useMovie(movieId)!;
|
||||
|
||||
const { isSaving, saveError, pendingChanges } = useSelector(
|
||||
(state: AppState) => state.movies
|
||||
);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const isPathChanging = pendingChanges.path && path !== pendingChanges.path;
|
||||
|
||||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const { settings, ...otherSettings } = useMemo(() => {
|
||||
return selectSettings(
|
||||
{
|
||||
monitored,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
path,
|
||||
tags,
|
||||
},
|
||||
pendingChanges,
|
||||
saveError
|
||||
);
|
||||
}, [
|
||||
monitored,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
path,
|
||||
tags,
|
||||
pendingChanges,
|
||||
saveError,
|
||||
]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
// @ts-expect-error actions aren't typed
|
||||
dispatch(setMovieValue({ name, value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
if (isPathChanging && !isConfirmMoveModalOpen) {
|
||||
setIsConfirmMoveModalOpen(true);
|
||||
} else {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveMovie({
|
||||
id: movieId,
|
||||
moveFiles: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [movieId, isPathChanging, isConfirmMoveModalOpen, dispatch]);
|
||||
|
||||
const handleMoveMoviePress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveMovie({
|
||||
id: movieId,
|
||||
moveFiles: true,
|
||||
})
|
||||
);
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && wasSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('EditMovieModalHeader', { title })}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherSettings}>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredMovieHelpText')}
|
||||
{...settings.monitored}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={<Icon className={styles.labelIcon} name={icons.INFO} />}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...settings.minimumAvailability}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...settings.qualityProfileId}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Path')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="path"
|
||||
{...settings.path}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...settings.tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteMoviePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
error={saveError}
|
||||
isSpinning={isSaving}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveMovieModal
|
||||
originalPath={path}
|
||||
destinationPath={pendingChanges.path}
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
onModalClose={handleCancelPress}
|
||||
onSavePress={handleSavePress}
|
||||
onMoveMoviePress={handleMoveMoviePress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditMovieModalContent;
|
||||
@@ -1,115 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import EditMovieModalContent from './EditMovieModalContent';
|
||||
|
||||
function createIsPathChangingSelector() {
|
||||
return createSelector(
|
||||
(state) => state.movies.pendingChanges,
|
||||
createMovieSelector(),
|
||||
(pendingChanges, movie) => {
|
||||
const path = pendingChanges.path;
|
||||
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return movie.path !== path;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies,
|
||||
createMovieSelector(),
|
||||
createIsPathChangingSelector(),
|
||||
(moviesState, movie, isPathChanging) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = moviesState;
|
||||
|
||||
const movieSettings = {
|
||||
monitored: movie.monitored,
|
||||
qualityProfileId: movie.qualityProfileId,
|
||||
minimumAvailability: movie.minimumAvailability,
|
||||
path: movie.path,
|
||||
tags: movie.tags
|
||||
};
|
||||
|
||||
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
isSaving,
|
||||
saveError,
|
||||
isPathChanging,
|
||||
originalPath: movie.path,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetMovieValue: setMovieValue,
|
||||
dispatchSaveMovie: saveMovie
|
||||
};
|
||||
|
||||
class EditMovieModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetMovieValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = (moveFiles) => {
|
||||
this.props.dispatchSaveMovie({
|
||||
id: this.props.movieId,
|
||||
moveFiles
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditMovieModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditMovieModalContentConnector.propTypes = {
|
||||
movieId: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchSetMovieValue: PropTypes.func.isRequired,
|
||||
dispatchSaveMovie: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditMovieModalContentConnector);
|
||||
@@ -1,33 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
|
||||
|
||||
function MovieHistoryModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
>
|
||||
<MovieHistoryModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MovieHistoryModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryModal;
|
||||
28
frontend/src/Movie/History/MovieHistoryModal.tsx
Normal file
28
frontend/src/Movie/History/MovieHistoryModal.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import MovieHistoryModalContent, {
|
||||
MovieHistoryModalContentProps,
|
||||
} from 'Movie/History/MovieHistoryModalContent';
|
||||
|
||||
interface MovieHistoryModalProps extends MovieHistoryModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function MovieHistoryModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: MovieHistoryModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<MovieHistoryModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieHistoryModal;
|
||||
@@ -1,141 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'eventType',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class MovieHistoryModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('History')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div>{translate('NoHistory')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && hasItems && !error &&
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<MovieHistoryRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryModalContent;
|
||||
148
frontend/src/Movie/History/MovieHistoryModalContent.tsx
Normal file
148
frontend/src/Movie/History/MovieHistoryModalContent.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearMovieHistory,
|
||||
fetchMovieHistory,
|
||||
movieHistoryMarkAsFailed,
|
||||
} from 'Store/Actions/movieHistoryActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieHistoryRow from './MovieHistoryRow';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export interface MovieHistoryModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function MovieHistoryModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
}: MovieHistoryModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.movieHistory
|
||||
);
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(
|
||||
(historyId: number) => {
|
||||
dispatch(
|
||||
movieHistoryMarkAsFailed({
|
||||
historyId,
|
||||
movieId,
|
||||
})
|
||||
);
|
||||
},
|
||||
[movieId, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
|
||||
return () => {
|
||||
dispatch(clearMovieHistory());
|
||||
};
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('History')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !hasItems && !error ? (
|
||||
<div>{translate('NoHistory')}</div>
|
||||
) : null}
|
||||
|
||||
{isPopulated && hasItems && !error && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<MovieHistoryRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieHistoryModalContent;
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
|
||||
import MovieHistoryModalContent from './MovieHistoryModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieHistory,
|
||||
(movieHistory) => {
|
||||
return movieHistory;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchMovieHistory,
|
||||
clearMovieHistory,
|
||||
movieHistoryMarkAsFailed
|
||||
};
|
||||
|
||||
class MovieHistoryModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchMovieHistory({
|
||||
movieId
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearMovieHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = (historyId) => {
|
||||
const {
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.movieHistoryMarkAsFailed({
|
||||
historyId,
|
||||
movieId
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieHistoryModalContent
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryModalContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
fetchMovieHistory: PropTypes.func.isRequired,
|
||||
clearMovieHistory: PropTypes.func.isRequired,
|
||||
movieHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);
|
||||
@@ -1,181 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieHistoryRow.css';
|
||||
|
||||
class MovieHistoryRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isMarkAsFailedModalOpen: false,
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: true });
|
||||
};
|
||||
|
||||
onConfirmMarkAsFailed = () => {
|
||||
this.props.onMarkAsFailedPress(this.props.id);
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
};
|
||||
|
||||
onMarkAsFailedModalClose = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
};
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
qualityCutoffNotMet,
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isMarkAsFailedModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<HistoryEventTypeCell
|
||||
eventType={eventType}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.sourceTitle}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCell
|
||||
date={date}
|
||||
includeSeconds={true}
|
||||
includeTime={true}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('MarkAsFailed')}
|
||||
message={translate('MarkAsFailedMessageText', [sourceTitle])}
|
||||
confirmLabel={translate('MarkAsFailed')}
|
||||
onConfirm={this.onConfirmMarkAsFailed}
|
||||
onCancel={this.onMarkAsFailedModalClose}
|
||||
/>
|
||||
|
||||
<HistoryDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
movie: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryRow;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user