From 8ec60eb0a63e5c63fab012a7662f44b5e7b2f614 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 7 Mar 2025 13:53:50 +0200 Subject: [PATCH] Convert Movie Formats/Status/CollectionLabel to TypeScript --- .../src/App/State/MovieCollectionAppState.ts | 8 ++- .../Interactive/InteractiveImportRow.tsx | 3 +- .../InteractiveImport/InteractiveImport.ts | 3 +- frontend/src/Movie/Details/MovieDetails.js | 4 +- frontend/src/Movie/Movie.ts | 1 + frontend/src/Movie/MovieCollectionLabel.js | 46 --------------- frontend/src/Movie/MovieCollectionLabel.tsx | 46 +++++++++++++++ .../Movie/MovieCollectionLabelConnector.js | 57 ------------------- frontend/src/Movie/MovieFormats.js | 33 ----------- frontend/src/Movie/MovieFormats.tsx | 22 +++++++ .../Movie/{MovieStatus.js => MovieStatus.tsx} | 48 +++++++--------- frontend/src/Movie/MovieStatusConnector.js | 50 ---------------- frontend/src/Movie/useMovie.ts | 9 ++- .../Selectors/createCollectionSelector.ts | 9 +++ .../Selectors/createQueueItemSelector.ts | 13 +++++ .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 4 +- frontend/src/Wanted/Missing/MissingRow.js | 4 +- frontend/src/typings/MovieCollection.ts | 1 + 18 files changed, 137 insertions(+), 224 deletions(-) delete mode 100644 frontend/src/Movie/MovieCollectionLabel.js create mode 100644 frontend/src/Movie/MovieCollectionLabel.tsx delete mode 100644 frontend/src/Movie/MovieCollectionLabelConnector.js delete mode 100644 frontend/src/Movie/MovieFormats.js create mode 100644 frontend/src/Movie/MovieFormats.tsx rename frontend/src/Movie/{MovieStatus.js => MovieStatus.tsx} (66%) delete mode 100644 frontend/src/Movie/MovieStatusConnector.js diff --git a/frontend/src/App/State/MovieCollectionAppState.ts b/frontend/src/App/State/MovieCollectionAppState.ts index 06d143674e..b8bf69ed86 100644 --- a/frontend/src/App/State/MovieCollectionAppState.ts +++ b/frontend/src/App/State/MovieCollectionAppState.ts @@ -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 { +interface MovieCollectionAppState + extends AppSectionState, + AppSectionSaveState { itemMap: Record; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 9abbaed134..2abcf9100b 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -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[]; diff --git a/frontend/src/InteractiveImport/InteractiveImport.ts b/frontend/src/InteractiveImport/InteractiveImport.ts index 4e876f8529..518092cdd2 100644 --- a/frontend/src/InteractiveImport/InteractiveImport.ts +++ b/frontend/src/InteractiveImport/InteractiveImport.ts @@ -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; diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index d03f7e9c8b..edf9bdcee6 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -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} >
-
diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index 96a334d7d6..30e622712e 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -79,6 +79,7 @@ interface Movie extends ModelBase { images: Image[]; movieFile: MovieFile; hasFile: boolean; + grabbed?: boolean; lastSearchTime?: string; isAvailable: boolean; isSaving?: boolean; diff --git a/frontend/src/Movie/MovieCollectionLabel.js b/frontend/src/Movie/MovieCollectionLabel.js deleted file mode 100644 index fb071f91c7..0000000000 --- a/frontend/src/Movie/MovieCollectionLabel.js +++ /dev/null @@ -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 ( -
- - {title} -
- ); - } -} - -MovieCollectionLabel.propTypes = { - title: PropTypes.string.isRequired, - monitored: PropTypes.bool.isRequired, - onMonitorTogglePress: PropTypes.func.isRequired -}; - -export default MovieCollectionLabel; diff --git a/frontend/src/Movie/MovieCollectionLabel.tsx b/frontend/src/Movie/MovieCollectionLabel.tsx new file mode 100644 index 0000000000..9d0c4f0227 --- /dev/null +++ b/frontend/src/Movie/MovieCollectionLabel.tsx @@ -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 ( +
+ + {title} +
+ ); +} + +export default MovieCollectionLabel; diff --git a/frontend/src/Movie/MovieCollectionLabelConnector.js b/frontend/src/Movie/MovieCollectionLabelConnector.js deleted file mode 100644 index 3d41e51e51..0000000000 --- a/frontend/src/Movie/MovieCollectionLabelConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -MovieCollectionLabelConnector.propTypes = { - tmdbId: PropTypes.number.isRequired, - id: PropTypes.number.isRequired, - monitored: PropTypes.bool.isRequired, - toggleCollectionMonitored: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector); diff --git a/frontend/src/Movie/MovieFormats.js b/frontend/src/Movie/MovieFormats.js deleted file mode 100644 index 9e8051be33..0000000000 --- a/frontend/src/Movie/MovieFormats.js +++ /dev/null @@ -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 ( -
- { - formats.map((format) => { - return ( - - ); - }) - } -
- ); -} - -MovieFormats.propTypes = { - formats: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -MovieFormats.defaultProps = { - formats: [] -}; - -export default MovieFormats; diff --git a/frontend/src/Movie/MovieFormats.tsx b/frontend/src/Movie/MovieFormats.tsx new file mode 100644 index 0000000000..5a8d5d4beb --- /dev/null +++ b/frontend/src/Movie/MovieFormats.tsx @@ -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 ( +
+ {formats.map(({ id, name }) => ( + + ))} +
+ ); +} + +export default MovieFormats; diff --git a/frontend/src/Movie/MovieStatus.js b/frontend/src/Movie/MovieStatus.tsx similarity index 66% rename from frontend/src/Movie/MovieStatus.js rename to frontend/src/Movie/MovieStatus.tsx index be54b63801..f8f35d5c0b 100644 --- a/frontend/src/Movie/MovieStatus.js +++ b/frontend/src/Movie/MovieStatus.tsx @@ -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 (
@@ -86,30 +94,16 @@ function MovieStatus(props) { if (isAvailable) { return (
- +
); } return (
- +
); } -MovieStatus.propTypes = { - isAvailable: PropTypes.bool.isRequired, - monitored: PropTypes.bool.isRequired, - grabbed: PropTypes.bool, - queueItem: PropTypes.object, - movieFile: PropTypes.object -}; - export default MovieStatus; diff --git a/frontend/src/Movie/MovieStatusConnector.js b/frontend/src/Movie/MovieStatusConnector.js deleted file mode 100644 index 25b104d35b..0000000000 --- a/frontend/src/Movie/MovieStatusConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -MovieStatusConnector.propTypes = { - movieId: PropTypes.number.isRequired, - movieFileId: PropTypes.number.isRequired -}; - -export default connect(createMapStateToProps, null)(MovieStatusConnector); diff --git a/frontend/src/Movie/useMovie.ts b/frontend/src/Movie/useMovie.ts index c1f8cedb87..50a51cc091 100644 --- a/frontend/src/Movie/useMovie.ts +++ b/frontend/src/Movie/useMovie.ts @@ -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)); } diff --git a/frontend/src/Store/Selectors/createCollectionSelector.ts b/frontend/src/Store/Selectors/createCollectionSelector.ts index 9b05202272..caab9ae6d5 100644 --- a/frontend/src/Store/Selectors/createCollectionSelector.ts +++ b/frontend/src/Store/Selectors/createCollectionSelector.ts @@ -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, diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.ts b/frontend/src/Store/Selectors/createQueueItemSelector.ts index ab161f5ee2..4a6afe5c67 100644 --- a/frontend/src/Store/Selectors/createQueueItemSelector.ts +++ b/frontend/src/Store/Selectors/createQueueItemSelector.ts @@ -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, diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index eb83e34dd2..43c9ab9fbc 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -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} > - -