Convert Movie Formats/Status/CollectionLabel to TypeScript

This commit is contained in:
Bogdan 2025-03-07 13:53:50 +02:00
parent 102849a697
commit 8ec60eb0a6
18 changed files with 137 additions and 224 deletions

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ 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 MovieCollectionLabel from 'Movie/MovieCollectionLabel';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
@ -609,7 +609,7 @@ class MovieDetails extends Component {
size={sizes.LARGE}
>
<div className={styles.collection}>
<MovieCollectionLabelConnector
<MovieCollectionLabel
tmdbId={collection.tmdbId}
/>
</div>

View file

@ -79,6 +79,7 @@ interface Movie extends ModelBase {
images: Image[];
movieFile: MovieFile;
hasFile: boolean;
grabbed?: boolean;
lastSearchTime?: string;
isAvailable: boolean;
isSaving?: boolean;

View file

@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import styles from './MovieCollectionLabel.css';
class MovieCollectionLabel extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false
};
}
render() {
const {
title,
monitored,
onMonitorTogglePress
} = this.props;
return (
<div>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={15}
onPress={onMonitorTogglePress}
/>
{title}
</div>
);
}
}
MovieCollectionLabel.propTypes = {
title: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired
};
export default MovieCollectionLabel;

View file

@ -0,0 +1,46 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
import { createCollectionSelectorForHook } from 'Store/Selectors/createCollectionSelector';
import MovieCollection from 'typings/MovieCollection';
import styles from './MovieCollectionLabel.css';
interface MovieCollectionLabelProps {
tmdbId: number;
}
function MovieCollectionLabel({ tmdbId }: MovieCollectionLabelProps) {
const {
id,
monitored,
title,
isSaving = false,
} = useSelector(createCollectionSelectorForHook(tmdbId)) as MovieCollection;
const dispatch = useDispatch();
const handleMonitorTogglePress = useCallback(
(value: boolean) => {
dispatch(
toggleCollectionMonitored({ collectionId: id, monitored: value })
);
},
[id, dispatch]
);
return (
<div>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
isSaving={isSaving}
size={15}
onPress={handleMonitorTogglePress}
/>
{title}
</div>
);
}
export default MovieCollectionLabel;

View file

@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
import MovieCollectionLabel from './MovieCollectionLabel';
function createMapStateToProps() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.movieCollections.items,
(tmdbId, collections) => {
const collection = collections.find((movie) => movie.tmdbId === tmdbId);
return {
...collection
};
}
);
}
const mapDispatchToProps = {
toggleCollectionMonitored
};
class MovieCollectionLabelConnector extends Component {
//
// Listeners
onMonitorTogglePress = (monitored) => {
this.props.toggleCollectionMonitored({
collectionId: this.props.id,
monitored
});
};
//
// Render
render() {
return (
<MovieCollectionLabel
{...this.props}
onMonitorTogglePress={this.onMonitorTogglePress}
/>
);
}
}
MovieCollectionLabelConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleCollectionMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector);

View file

@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
function MovieFormats({ formats }) {
return (
<div>
{
formats.map((format) => {
return (
<Label
key={format.id}
kind={kinds.INFO}
>
{format.name}
</Label>
);
})
}
</div>
);
}
MovieFormats.propTypes = {
formats: PropTypes.arrayOf(PropTypes.object).isRequired
};
MovieFormats.defaultProps = {
formats: []
};
export default MovieFormats;

View file

@ -0,0 +1,22 @@
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import CustomFormat from 'typings/CustomFormat';
interface MovieFormatsProps {
formats: CustomFormat[];
}
function MovieFormats({ formats }: MovieFormatsProps) {
return (
<div>
{formats.map(({ id, name }) => (
<Label key={id} kind={kinds.INFO}>
{name}
</Label>
))}
</div>
);
}
export default MovieFormats;

View file

@ -1,32 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import QueueDetails from 'Activity/Queue/QueueDetails';
import Icon from 'Components/Icon';
import ProgressBar from 'Components/ProgressBar';
import { icons, kinds, sizes } from 'Helpers/Props';
import Movie from 'Movie/Movie';
import useMovie, { MovieEntity } from 'Movie/useMovie';
import useMovieFile from 'MovieFile/useMovieFile';
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
import translate from 'Utilities/String/translate';
import MovieQuality from './MovieQuality';
import styles from './MovieStatus.css';
function MovieStatus(props) {
interface MovieStatusProps {
movieId: number;
movieEntity?: MovieEntity;
movieFileId: number | undefined;
}
function MovieStatus({ movieId, movieFileId }: MovieStatusProps) {
const {
isAvailable,
monitored,
grabbed,
queueItem,
movieFile
} = props;
grabbed = false,
} = useMovie(movieId) as Movie;
const queueItem = useSelector(createQueueItemSelectorForHook(movieId));
const movieFile = useMovieFile(movieFileId);
const hasMovieFile = !!movieFile;
const isQueued = !!queueItem;
if (isQueued) {
const {
sizeleft,
size
} = queueItem;
const { sizeleft, size } = queueItem;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
return (
<div className={styles.center}>
@ -86,30 +94,16 @@ function MovieStatus(props) {
if (isAvailable) {
return (
<div className={styles.center}>
<Icon
name={icons.MISSING}
title={translate('MovieMissingFromDisk')}
/>
<Icon name={icons.MISSING} title={translate('MovieMissingFromDisk')} />
</div>
);
}
return (
<div className={styles.center}>
<Icon
name={icons.NOT_AIRED}
title={translate('MovieIsNotAvailable')}
/>
<Icon name={icons.NOT_AIRED} title={translate('MovieIsNotAvailable')} />
</div>
);
}
MovieStatus.propTypes = {
isAvailable: PropTypes.bool.isRequired,
monitored: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
movieFile: PropTypes.object
};
export default MovieStatus;

View file

@ -1,50 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieStatus from 'Movie/MovieStatus';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
function createMapStateToProps() {
return createSelector(
createMovieByEntitySelector(),
createQueueItemSelector(),
createMovieFileSelector(),
(movie, queueItem, movieFile) => {
const result = _.pick(movie, [
'isAvailable',
'monitored',
'grabbed'
]);
result.queueItem = queueItem;
result.movieFile = movieFile;
return result;
}
);
}
class MovieStatusConnector extends Component {
//
// Render
render() {
return (
<MovieStatus
{...this.props}
/>
);
}
}
MovieStatusConnector.propTypes = {
movieId: PropTypes.number.isRequired,
movieFileId: PropTypes.number.isRequired
};
export default connect(createMapStateToProps, null)(MovieStatusConnector);

View file

@ -2,6 +2,13 @@ import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export type MovieEntity =
| 'calendar'
| 'movies'
| 'interactiveImport.movies'
| 'wanted.cutoffUnmet'
| 'wanted.missing';
export function createMovieSelector(movieId?: number) {
return createSelector(
(state: AppState) => state.movies.itemMap,
@ -12,7 +19,7 @@ export function createMovieSelector(movieId?: number) {
);
}
function useMovie(movieId?: number) {
function useMovie(movieId: number | undefined) {
return useSelector(createMovieSelector(movieId));
}

View file

@ -1,6 +1,15 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createCollectionSelectorForHook(tmdbId: number) {
return createSelector(
(state: AppState) => state.movieCollections.items,
(collections) => {
return collections.find((item) => item.tmdbId === tmdbId);
}
);
}
function createCollectionSelector() {
return createSelector(
(_: AppState, { collectionId }: { collectionId: number }) => collectionId,

View file

@ -1,6 +1,19 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createQueueItemSelectorForHook(movieId: number) {
return createSelector(
(state: AppState) => state.queue.details.items,
(details) => {
if (!movieId || !details) {
return null;
}
return details.find((item) => item.movieId === movieId);
}
);
}
function createQueueItemSelector() {
return createSelector(
(_: AppState, { movieId }: { movieId: number }) => movieId,

View file

@ -6,7 +6,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
import styles from './CutoffUnmetRow.css';
@ -127,7 +127,7 @@ function CutoffUnmetRow(props) {
key={name}
className={styles.status}
>
<MovieStatusConnector
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}

View file

@ -6,7 +6,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import styles from './MissingRow.css';
@ -117,7 +117,7 @@ function MissingRow(props) {
key={name}
className={styles.status}
>
<MovieStatusConnector
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity={movieEntities.WANTED_MISSING}

View file

@ -12,6 +12,7 @@ interface MovieCollection extends ModelBase {
movies: Movie[];
missingMovies: number;
tags: number[];
isSaving?: boolean;
}
export default MovieCollection;