From 7644cec376a3b774e1a5752280b6f291aee46026 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 10 Jan 2025 20:42:42 -0800 Subject: [PATCH] Convert Series Details to TypeScript --- frontend/src/App/AppRoutes.tsx | 4 +- frontend/src/App/State/EpisodesAppState.ts | 5 +- frontend/src/Episode/EpisodeStatus.tsx | 2 +- .../src/EpisodeFile/EpisodeFileLanguages.tsx | 2 +- .../InteractiveImportModalContent.tsx | 2 +- .../InteractiveImportModal.tsx | 7 +- frontend/src/Series/Details/EpisodeRow.js | 422 --------- frontend/src/Series/Details/EpisodeRow.tsx | 338 +++++++ .../src/Series/Details/EpisodeRowConnector.js | 28 - .../Series/Details/SeasonProgressLabel.tsx | 3 + .../Series/Details/SeriesAlternateTitles.js | 32 - .../Series/Details/SeriesAlternateTitles.tsx | 28 + frontend/src/Series/Details/SeriesDetails.js | 786 --------------- frontend/src/Series/Details/SeriesDetails.tsx | 891 ++++++++++++++++++ .../Series/Details/SeriesDetailsConnector.js | 265 ------ .../src/Series/Details/SeriesDetailsPage.tsx | 34 + .../Details/SeriesDetailsPageConnector.js | 77 -- .../Series/Details/SeriesDetailsSeason.css | 24 +- .../Details/SeriesDetailsSeason.css.d.ts | 3 + .../src/Series/Details/SeriesDetailsSeason.js | 546 ----------- .../Series/Details/SeriesDetailsSeason.tsx | 573 +++++++++++ .../Details/SeriesDetailsSeasonConnector.js | 130 --- frontend/src/Series/Details/SeriesTags.js | 30 - frontend/src/Series/Details/SeriesTags.tsx | 35 + .../src/Series/Details/SeriesTagsConnector.js | 26 - frontend/src/Series/Series.ts | 1 + .../src/Utilities/Number/formatRuntime.ts | 2 +- 27 files changed, 1943 insertions(+), 2353 deletions(-) delete mode 100644 frontend/src/Series/Details/EpisodeRow.js create mode 100644 frontend/src/Series/Details/EpisodeRow.tsx delete mode 100644 frontend/src/Series/Details/EpisodeRowConnector.js delete mode 100644 frontend/src/Series/Details/SeriesAlternateTitles.js create mode 100644 frontend/src/Series/Details/SeriesAlternateTitles.tsx delete mode 100644 frontend/src/Series/Details/SeriesDetails.js create mode 100644 frontend/src/Series/Details/SeriesDetails.tsx delete mode 100644 frontend/src/Series/Details/SeriesDetailsConnector.js create mode 100644 frontend/src/Series/Details/SeriesDetailsPage.tsx delete mode 100644 frontend/src/Series/Details/SeriesDetailsPageConnector.js delete mode 100644 frontend/src/Series/Details/SeriesDetailsSeason.js create mode 100644 frontend/src/Series/Details/SeriesDetailsSeason.tsx delete mode 100644 frontend/src/Series/Details/SeriesDetailsSeasonConnector.js delete mode 100644 frontend/src/Series/Details/SeriesTags.js create mode 100644 frontend/src/Series/Details/SeriesTags.tsx delete mode 100644 frontend/src/Series/Details/SeriesTagsConnector.js diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 4ba7e16b0..b36415486 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -8,7 +8,7 @@ import ImportSeries from 'AddSeries/ImportSeries/ImportSeries'; import CalendarPage from 'Calendar/CalendarPage'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; -import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector'; +import SeriesDetailsPage from 'Series/Details/SeriesDetailsPage'; import SeriesIndex from 'Series/Index/SeriesIndex'; import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage'; import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings'; @@ -66,7 +66,7 @@ function AppRoutes() { - + {/* Calendar diff --git a/frontend/src/App/State/EpisodesAppState.ts b/frontend/src/App/State/EpisodesAppState.ts index 4234c0bcb..cb90adfc9 100644 --- a/frontend/src/App/State/EpisodesAppState.ts +++ b/frontend/src/App/State/EpisodesAppState.ts @@ -1,6 +1,9 @@ import AppSectionState from 'App/State/AppSectionState'; +import Column from 'Components/Table/Column'; import Episode from 'Episode/Episode'; -type EpisodesAppState = AppSectionState; +interface EpisodesAppState extends AppSectionState { + columns: Column[]; +} export default EpisodesAppState; diff --git a/frontend/src/Episode/EpisodeStatus.tsx b/frontend/src/Episode/EpisodeStatus.tsx index 8a2c943cb..9bdba0c4f 100644 --- a/frontend/src/Episode/EpisodeStatus.tsx +++ b/frontend/src/Episode/EpisodeStatus.tsx @@ -16,7 +16,7 @@ import styles from './EpisodeStatus.css'; interface EpisodeStatusProps { episodeId: number; episodeEntity?: EpisodeEntity; - episodeFileId: number; + episodeFileId: number | undefined; } function EpisodeStatus({ diff --git a/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx b/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx index c3ab2bbe1..110196cef 100644 --- a/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx +++ b/frontend/src/EpisodeFile/EpisodeFileLanguages.tsx @@ -3,7 +3,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages'; import useEpisodeFile from './useEpisodeFile'; interface EpisodeFileLanguagesProps { - episodeFileId: number; + episodeFileId: number | undefined; } function EpisodeFileLanguages({ episodeFileId }: EpisodeFileLanguagesProps) { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 922f4d5ac..75cafed5f 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -219,7 +219,7 @@ const importModeSelector = createSelector( } ); -interface InteractiveImportModalContentProps { +export interface InteractiveImportModalContentProps { downloadId?: string; seriesId?: number; seasonNumber?: number; diff --git a/frontend/src/InteractiveImport/InteractiveImportModal.tsx b/frontend/src/InteractiveImport/InteractiveImportModal.tsx index 11dc8e6ae..da2966160 100644 --- a/frontend/src/InteractiveImport/InteractiveImportModal.tsx +++ b/frontend/src/InteractiveImport/InteractiveImportModal.tsx @@ -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 { isOpen: boolean; folder?: string; downloadId?: string; diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js deleted file mode 100644 index a1dc3e21a..000000000 --- a/frontend/src/Series/Details/EpisodeRow.js +++ /dev/null @@ -1,422 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import MonitorToggleButton from 'Components/MonitorToggleButton'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -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 EpisodeFormats from 'Episode/EpisodeFormats'; -import EpisodeNumber from 'Episode/EpisodeNumber'; -import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; -import EpisodeStatus from 'Episode/EpisodeStatus'; -import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; -import IndexerFlags from 'Episode/IndexerFlags'; -import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages'; -import MediaInfo from 'EpisodeFile/MediaInfo'; -import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import formatBytes from 'Utilities/Number/formatBytes'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import formatRuntime from 'Utilities/Number/formatRuntime'; -import translate from 'Utilities/String/translate'; -import styles from './EpisodeRow.css'; - -class EpisodeRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - // - // Listeners - - onManualSearchPress = () => { - this.setState({ isDetailsModalOpen: true }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - onMonitorEpisodePress = (monitored, options) => { - this.props.onMonitorEpisodePress(this.props.id, monitored, options); - }; - - // - // Render - - render() { - const { - id, - seriesId, - episodeFileId, - monitored, - seasonNumber, - episodeNumber, - absoluteEpisodeNumber, - sceneSeasonNumber, - sceneEpisodeNumber, - sceneAbsoluteEpisodeNumber, - airDateUtc, - runtime, - finaleType, - title, - useSceneNumbering, - unverifiedSceneNumbering, - isSaving, - seriesMonitored, - seriesType, - episodeFilePath, - episodeFileRelativePath, - episodeFileSize, - releaseGroup, - customFormats, - customFormatScore, - indexerFlags, - alternateTitles, - columns - } = this.props; - - return ( - - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'monitored') { - return ( - - - - ); - } - - if (name === 'episodeNumber') { - return ( - - - - ); - } - - if (name === 'title') { - return ( - - - - ); - } - - if (name === 'path') { - return ( - - { - episodeFilePath - } - - ); - } - - if (name === 'relativePath') { - return ( - - { - episodeFileRelativePath - } - - ); - } - - if (name === 'airDateUtc') { - return ( - - ); - } - - if (name === 'runtime') { - return ( - - { formatRuntime(runtime) } - - ); - } - - if (name === 'customFormats') { - return ( - - - - ); - } - - if (name === 'customFormatScore') { - return ( - - } - position={tooltipPositions.LEFT} - /> - - ); - } - - if (name === 'languages') { - return ( - - - - ); - } - - if (name === 'audioInfo') { - return ( - - - - ); - } - - if (name === 'audioLanguages') { - return ( - - - - ); - } - - if (name === 'subtitleLanguages') { - return ( - - - - ); - } - - if (name === 'videoCodec') { - return ( - - - - ); - } - - if (name === 'videoDynamicRangeType') { - return ( - - - - ); - } - - if (name === 'size') { - return ( - - {!!episodeFileSize && formatBytes(episodeFileSize)} - - ); - } - - if (name === 'releaseGroup') { - return ( - - {releaseGroup} - - ); - } - - if (name === 'indexerFlags') { - return ( - - {indexerFlags ? ( - } - title={translate('IndexerFlags')} - body={} - position={tooltipPositions.LEFT} - /> - ) : null} - - ); - } - - if (name === 'status') { - return ( - - - - ); - } - - if (name === 'actions') { - return ( - - ); - } - - return null; - }) - } - - ); - } -} - -EpisodeRow.propTypes = { - id: PropTypes.number.isRequired, - seriesId: PropTypes.number.isRequired, - episodeFileId: PropTypes.number, - monitored: PropTypes.bool.isRequired, - seasonNumber: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number, - sceneSeasonNumber: PropTypes.number, - sceneEpisodeNumber: PropTypes.number, - sceneAbsoluteEpisodeNumber: PropTypes.number, - airDateUtc: PropTypes.string, - runtime: PropTypes.number, - finaleType: PropTypes.string, - title: PropTypes.string.isRequired, - isSaving: PropTypes.bool, - useSceneNumbering: PropTypes.bool, - unverifiedSceneNumbering: PropTypes.bool, - seriesMonitored: PropTypes.bool.isRequired, - seriesType: PropTypes.string.isRequired, - episodeFilePath: PropTypes.string, - episodeFileRelativePath: PropTypes.string, - episodeFileSize: PropTypes.number, - releaseGroup: PropTypes.string, - customFormats: PropTypes.arrayOf(PropTypes.object), - customFormatScore: PropTypes.number.isRequired, - indexerFlags: PropTypes.number.isRequired, - mediaInfo: PropTypes.object, - alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - onMonitorEpisodePress: PropTypes.func.isRequired -}; - -EpisodeRow.defaultProps = { - alternateTitles: [], - customFormats: [], - indexerFlags: 0 -}; - -export default EpisodeRow; diff --git a/frontend/src/Series/Details/EpisodeRow.tsx b/frontend/src/Series/Details/EpisodeRow.tsx new file mode 100644 index 000000000..fb3afb4d2 --- /dev/null +++ b/frontend/src/Series/Details/EpisodeRow.tsx @@ -0,0 +1,338 @@ +import React, { useCallback } from 'react'; +import Icon from 'Components/Icon'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import TableRow from 'Components/Table/TableRow'; +import Popover from 'Components/Tooltip/Popover'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import EpisodeFormats from 'Episode/EpisodeFormats'; +import EpisodeNumber from 'Episode/EpisodeNumber'; +import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; +import EpisodeStatus from 'Episode/EpisodeStatus'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import IndexerFlags from 'Episode/IndexerFlags'; +import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages'; +import MediaInfo from 'EpisodeFile/MediaInfo'; +import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; +import useEpisodeFile from 'EpisodeFile/useEpisodeFile'; +import { icons } from 'Helpers/Props'; +import useSeries from 'Series/useSeries'; +import MediaInfoModel from 'typings/MediaInfo'; +import formatBytes from 'Utilities/Number/formatBytes'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import formatRuntime from 'Utilities/Number/formatRuntime'; +import translate from 'Utilities/String/translate'; +import styles from './EpisodeRow.css'; + +interface EpisodeRowProps { + id: number; + seriesId: number; + episodeFileId?: number; + monitored: boolean; + seasonNumber: number; + episodeNumber: number; + absoluteEpisodeNumber?: number; + sceneSeasonNumber?: number; + sceneEpisodeNumber?: number; + sceneAbsoluteEpisodeNumber?: number; + airDateUtc?: string; + runtime?: number; + finaleType?: string; + title: string; + isSaving?: boolean; + unverifiedSceneNumbering?: boolean; + // episodeFilePath?: string; + // episodeFileRelativePath?: string; + // episodeFileSize?: number; + // releaseGroup?: string; + // customFormats?: CustomFormat[]; + // customFormatScore: number; + // indexerFlags?: number; + mediaInfo?: MediaInfoModel; + columns: Column[]; + onMonitorEpisodePress: ( + episodeId: number, + value: boolean, + { shiftKey }: { shiftKey: boolean } + ) => void; +} + +function EpisodeRow({ + id, + seriesId, + episodeFileId, + monitored, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + airDateUtc, + runtime, + finaleType, + title, + unverifiedSceneNumbering, + isSaving, + // episodeFilePath, + // episodeFileRelativePath, + // episodeFileSize, + // releaseGroup, + // customFormats = [], + // customFormatScore, + // indexerFlags = 0, + columns, + onMonitorEpisodePress, +}: EpisodeRowProps) { + const { + useSceneNumbering, + monitored: seriesMonitored, + seriesType, + alternateTitles = [], + } = useSeries(seriesId)!; + const episodeFile = useEpisodeFile(episodeFileId); + + const customFormats = episodeFile?.customFormats ?? []; + const customFormatScore = episodeFile?.customFormatScore ?? 0; + + const handleMonitorEpisodePress = useCallback( + (monitored: boolean, options: { shiftKey: boolean }) => { + onMonitorEpisodePress(id, monitored, options); + }, + [id, onMonitorEpisodePress] + ); + + return ( + + {columns.map((column) => { + const { name, isVisible } = column; + + if (!isVisible) { + return null; + } + + if (name === 'monitored') { + return ( + + + + ); + } + + if (name === 'episodeNumber') { + return ( + + + + ); + } + + if (name === 'title') { + return ( + + + + ); + } + + if (name === 'path') { + return {episodeFile?.path}; + } + + if (name === 'relativePath') { + return ( + {episodeFile?.relativePath} + ); + } + + if (name === 'airDateUtc') { + return ; + } + + if (name === 'runtime') { + return ( + + {formatRuntime(runtime)} + + ); + } + + if (name === 'customFormats') { + return ( + + + + ); + } + + if (name === 'customFormatScore') { + return ( + + } + position="left" + /> + + ); + } + + if (name === 'languages') { + return ( + + + + ); + } + + if (name === 'audioInfo') { + return ( + + + + ); + } + + if (name === 'audioLanguages') { + return ( + + + + ); + } + + if (name === 'subtitleLanguages') { + return ( + + + + ); + } + + if (name === 'videoCodec') { + return ( + + + + ); + } + + if (name === 'videoDynamicRangeType') { + return ( + + + + ); + } + + if (name === 'size') { + return ( + + {!!episodeFile?.size && formatBytes(episodeFile?.size)} + + ); + } + + if (name === 'releaseGroup') { + return ( + + {episodeFile?.releaseGroup} + + ); + } + + if (name === 'indexerFlags') { + return ( + + {episodeFile?.indexerFlags ? ( + } + title={translate('IndexerFlags')} + body={ + + } + position="left" + /> + ) : null} + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + ); + } + + return null; + })} + + ); +} + +export default EpisodeRow; diff --git a/frontend/src/Series/Details/EpisodeRowConnector.js b/frontend/src/Series/Details/EpisodeRowConnector.js deleted file mode 100644 index 59c6818d4..000000000 --- a/frontend/src/Series/Details/EpisodeRowConnector.js +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; -import createSeriesSelector from 'Store/Selectors/createSeriesSelector'; -import EpisodeRow from './EpisodeRow'; - -function createMapStateToProps() { - return createSelector( - createSeriesSelector(), - createEpisodeFileSelector(), - (series = {}, episodeFile) => { - return { - useSceneNumbering: series.useSceneNumbering, - seriesMonitored: series.monitored, - seriesType: series.seriesType, - episodeFilePath: episodeFile ? episodeFile.path : null, - episodeFileRelativePath: episodeFile ? episodeFile.relativePath : null, - episodeFileSize: episodeFile ? episodeFile.size : null, - releaseGroup: episodeFile ? episodeFile.releaseGroup : null, - customFormats: episodeFile ? episodeFile.customFormats : [], - customFormatScore: episodeFile ? episodeFile.customFormatScore : 0, - indexerFlags: episodeFile ? episodeFile.indexerFlags : 0, - alternateTitles: series.alternateTitles - }; - } - ); -} -export default connect(createMapStateToProps)(EpisodeRow); diff --git a/frontend/src/Series/Details/SeasonProgressLabel.tsx b/frontend/src/Series/Details/SeasonProgressLabel.tsx index 466d5c31b..fc15d07b4 100644 --- a/frontend/src/Series/Details/SeasonProgressLabel.tsx +++ b/frontend/src/Series/Details/SeasonProgressLabel.tsx @@ -28,6 +28,7 @@ function getEpisodeCountKind( } interface SeasonProgressLabelProps { + className: string; seriesId: number; seasonNumber: number; monitored: boolean; @@ -36,6 +37,7 @@ interface SeasonProgressLabelProps { } function SeasonProgressLabel({ + className, seriesId, seasonNumber, monitored, @@ -53,6 +55,7 @@ function SeasonProgressLabel({ return (