From 89815c1d7a52a97008903e202cd5942f44bb5c06 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 29 Dec 2025 16:09:21 -0600 Subject: [PATCH] feat(tv): add TV show frontend components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TVShow, Season, Episode TypeScript types - Add TV state management (actions, reducers, selectors) - Add TVShowIndex and TVShowIndexRow components - Add TVShowDetails and TVShowDetailsPage components - Add TV routes to AppRoutes - Add TV Shows sidebar navigation - Add TV icon to icons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/App/AppRoutes.tsx | 10 ++ frontend/src/App/State/AppState.ts | 6 + frontend/src/App/State/EpisodesAppState.ts | 12 ++ frontend/src/App/State/SeasonsAppState.ts | 12 ++ frontend/src/App/State/TVShowsAppState.ts | 12 ++ .../Components/Page/Sidebar/PageSidebar.tsx | 6 + frontend/src/Helpers/Props/icons.ts | 2 + frontend/src/Store/Actions/episodeActions.js | 50 +++++++ frontend/src/Store/Actions/index.js | 6 + frontend/src/Store/Actions/seasonActions.js | 50 +++++++ frontend/src/Store/Actions/tvShowActions.js | 50 +++++++ .../Selectors/createAllTVShowsSelector.ts | 13 ++ frontend/src/TVShow/Details/TVShowDetails.css | 58 +++++++++ .../src/TVShow/Details/TVShowDetails.css.d.ts | 17 +++ frontend/src/TVShow/Details/TVShowDetails.tsx | 123 ++++++++++++++++++ .../src/TVShow/Details/TVShowDetailsPage.tsx | 37 ++++++ frontend/src/TVShow/Episode.ts | 30 +++++ frontend/src/TVShow/Index/TVShowIndex.tsx | 103 +++++++++++++++ frontend/src/TVShow/Index/TVShowIndexRow.tsx | 30 +++++ frontend/src/TVShow/Season.ts | 12 ++ frontend/src/TVShow/TVShow.ts | 39 ++++++ 21 files changed, 678 insertions(+) create mode 100644 frontend/src/App/State/EpisodesAppState.ts create mode 100644 frontend/src/App/State/SeasonsAppState.ts create mode 100644 frontend/src/App/State/TVShowsAppState.ts create mode 100644 frontend/src/Store/Actions/episodeActions.js create mode 100644 frontend/src/Store/Actions/seasonActions.js create mode 100644 frontend/src/Store/Actions/tvShowActions.js create mode 100644 frontend/src/Store/Selectors/createAllTVShowsSelector.ts create mode 100644 frontend/src/TVShow/Details/TVShowDetails.css create mode 100644 frontend/src/TVShow/Details/TVShowDetails.css.d.ts create mode 100644 frontend/src/TVShow/Details/TVShowDetails.tsx create mode 100644 frontend/src/TVShow/Details/TVShowDetailsPage.tsx create mode 100644 frontend/src/TVShow/Episode.ts create mode 100644 frontend/src/TVShow/Index/TVShowIndex.tsx create mode 100644 frontend/src/TVShow/Index/TVShowIndexRow.tsx create mode 100644 frontend/src/TVShow/Season.ts create mode 100644 frontend/src/TVShow/TVShow.ts diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 9510f8c371..2dadb22339 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -42,6 +42,8 @@ import Logs from 'System/Logs/Logs'; import Status from 'System/Status/Status'; import Tasks from 'System/Tasks/Tasks'; import Updates from 'System/Updates/Updates'; +import TVShowDetailsPage from 'TVShow/Details/TVShowDetailsPage'; +import TVShowIndex from 'TVShow/Index/TVShowIndex'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet'; import Missing from 'Wanted/Missing/Missing'; @@ -124,6 +126,14 @@ function AppRoutes() { + {/* + TV Shows + */} + + + + + {/* Calendar */} diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 1eb0df00a4..2879859081 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -11,6 +11,7 @@ import CaptchaAppState from './CaptchaAppState'; import CommandAppState from './CommandAppState'; import CustomFiltersAppState from './CustomFiltersAppState'; import DashboardAppState from './DashboardAppState'; +import EpisodesAppState from './EpisodesAppState'; import ExtraFilesAppState from './ExtraFilesAppState'; import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState'; import InteractiveImportAppState from './InteractiveImportAppState'; @@ -28,9 +29,11 @@ import ProviderOptionsAppState from './ProviderOptionsAppState'; import QueueAppState from './QueueAppState'; import ReleasesAppState from './ReleasesAppState'; import RootFolderAppState from './RootFolderAppState'; +import SeasonsAppState from './SeasonsAppState'; import SettingsAppState from './SettingsAppState'; import SystemAppState from './SystemAppState'; import TagsAppState from './TagsAppState'; +import TVShowsAppState from './TVShowsAppState'; import WantedAppState from './WantedAppState'; interface FilterBuilderPropOption { @@ -99,6 +102,7 @@ interface AppState { commands: CommandAppState; customFilters: CustomFiltersAppState; dashboard: DashboardAppState; + episodes: EpisodesAppState; extraFiles: ExtraFilesAppState; history: HistoryAppState; interactiveImport: InteractiveImportAppState; @@ -118,9 +122,11 @@ interface AppState { releases: ReleasesAppState; rootFolders: RootFolderAppState; bookSeries: BookSeriesAppState; + seasons: SeasonsAppState; settings: SettingsAppState; system: SystemAppState; tags: TagsAppState; + tvShows: TVShowsAppState; wanted: WantedAppState; } diff --git a/frontend/src/App/State/EpisodesAppState.ts b/frontend/src/App/State/EpisodesAppState.ts new file mode 100644 index 0000000000..24d9ec7e93 --- /dev/null +++ b/frontend/src/App/State/EpisodesAppState.ts @@ -0,0 +1,12 @@ +import AppSectionState, { + AppSectionDeleteState, + AppSectionSaveState, +} from 'App/State/AppSectionState'; +import Episode from 'TVShow/Episode'; + +interface EpisodesAppState + extends AppSectionState, AppSectionDeleteState, AppSectionSaveState { + pendingChanges: Partial; +} + +export default EpisodesAppState; diff --git a/frontend/src/App/State/SeasonsAppState.ts b/frontend/src/App/State/SeasonsAppState.ts new file mode 100644 index 0000000000..782aa4b9e9 --- /dev/null +++ b/frontend/src/App/State/SeasonsAppState.ts @@ -0,0 +1,12 @@ +import AppSectionState, { + AppSectionDeleteState, + AppSectionSaveState, +} from 'App/State/AppSectionState'; +import Season from 'TVShow/Season'; + +interface SeasonsAppState + extends AppSectionState, AppSectionDeleteState, AppSectionSaveState { + pendingChanges: Partial; +} + +export default SeasonsAppState; diff --git a/frontend/src/App/State/TVShowsAppState.ts b/frontend/src/App/State/TVShowsAppState.ts new file mode 100644 index 0000000000..dffcbc0484 --- /dev/null +++ b/frontend/src/App/State/TVShowsAppState.ts @@ -0,0 +1,12 @@ +import AppSectionState, { + AppSectionDeleteState, + AppSectionSaveState, +} from 'App/State/AppSectionState'; +import TVShow from 'TVShow/TVShow'; + +interface TVShowsAppState + extends AppSectionState, AppSectionDeleteState, AppSectionSaveState { + pendingChanges: Partial; +} + +export default TVShowsAppState; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx index d375d121f5..b1f6dcbb8d 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx @@ -119,6 +119,12 @@ const LINKS: SidebarItem[] = [ to: '/bookseries', }, + { + iconName: icons.TV, + title: () => translate('TVShows'), + to: '/tvshows', + }, + { iconName: icons.CALENDAR, title: () => translate('Calendar'), diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index 4d4f27393c..26b72d0209 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -117,6 +117,7 @@ import { faTimes as fasTimes, faTimesCircle as fasTimesCircle, faTrashAlt as fasTrashAlt, + faTv as fasTv, faUser as fasUser, faUserPlus as fasUserPlus, faVial as fasVial, @@ -258,3 +259,4 @@ export const WARNING = fasExclamationTriangle; export const WIKI = fasBookReader; export const BLOCKLIST = fasBan; export const BOOK = fasBook; +export const TV = fasTv; diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js new file mode 100644 index 0000000000..8a4a94ad69 --- /dev/null +++ b/frontend/src/Store/Actions/episodeActions.js @@ -0,0 +1,50 @@ +import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; + +export const section = 'episodes'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + items: [], + sortKey: 'episodeNumber', + sortDirection: sortDirections.ASCENDING, + pendingChanges: {} +}; + +export const FETCH_EPISODES = 'episodes/fetchEpisodes'; +export const SET_EPISODE_VALUE = 'episodes/setEpisodeValue'; +export const SAVE_EPISODE = 'episodes/saveEpisode'; +export const DELETE_EPISODE = 'episodes/deleteEpisode'; + +export const fetchEpisodes = createThunk(FETCH_EPISODES); +export const saveEpisode = createThunk(SAVE_EPISODE); +export const deleteEpisode = createThunk(DELETE_EPISODE); + +export const setEpisodeValue = createAction(SET_EPISODE_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const actionHandlers = handleThunks({ + [FETCH_EPISODES]: createFetchHandler(section, '/episode'), + [SAVE_EPISODE]: createSaveProviderHandler(section, '/episode'), + [DELETE_EPISODE]: createRemoveItemHandler(section, '/episode') +}); + +export const reducers = createHandleActions({ + [SET_EPISODE_VALUE]: createSetSettingValueReducer(section) +}, defaultState, section); diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 77c1b1c12f..6390cc78ed 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -12,6 +12,7 @@ import * as captcha from './captchaActions'; import * as commands from './commandActions'; import * as customFilters from './customFilterActions'; import * as discoverMovie from './discoverMovieActions'; +import * as episodes from './episodeActions'; import * as extraFiles from './extraFileActions'; import * as history from './historyActions'; import * as importMovie from './importMovieActions'; @@ -32,9 +33,11 @@ import * as queue from './queueActions'; import * as releases from './releaseActions'; import * as rootFolders from './rootFolderActions'; import * as bookSeries from './bookSeriesActions'; +import * as seasons from './seasonActions'; import * as settings from './settingsActions'; import * as system from './systemActions'; import * as tags from './tagActions'; +import * as tvShows from './tvShowActions'; import * as wanted from './wantedActions'; export default [ @@ -52,6 +55,7 @@ export default [ commands, customFilters, discoverMovie, + episodes, movieFiles, extraFiles, history, @@ -72,8 +76,10 @@ export default [ movieIndex, movieCredits, bookSeries, + seasons, settings, system, tags, + tvShows, wanted ]; diff --git a/frontend/src/Store/Actions/seasonActions.js b/frontend/src/Store/Actions/seasonActions.js new file mode 100644 index 0000000000..6ba2c4de80 --- /dev/null +++ b/frontend/src/Store/Actions/seasonActions.js @@ -0,0 +1,50 @@ +import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; + +export const section = 'seasons'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + items: [], + sortKey: 'seasonNumber', + sortDirection: sortDirections.ASCENDING, + pendingChanges: {} +}; + +export const FETCH_SEASONS = 'seasons/fetchSeasons'; +export const SET_SEASON_VALUE = 'seasons/setSeasonValue'; +export const SAVE_SEASON = 'seasons/saveSeason'; +export const DELETE_SEASON = 'seasons/deleteSeason'; + +export const fetchSeasons = createThunk(FETCH_SEASONS); +export const saveSeason = createThunk(SAVE_SEASON); +export const deleteSeason = createThunk(DELETE_SEASON); + +export const setSeasonValue = createAction(SET_SEASON_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const actionHandlers = handleThunks({ + [FETCH_SEASONS]: createFetchHandler(section, '/season'), + [SAVE_SEASON]: createSaveProviderHandler(section, '/season'), + [DELETE_SEASON]: createRemoveItemHandler(section, '/season') +}); + +export const reducers = createHandleActions({ + [SET_SEASON_VALUE]: createSetSettingValueReducer(section) +}, defaultState, section); diff --git a/frontend/src/Store/Actions/tvShowActions.js b/frontend/src/Store/Actions/tvShowActions.js new file mode 100644 index 0000000000..117dbf9671 --- /dev/null +++ b/frontend/src/Store/Actions/tvShowActions.js @@ -0,0 +1,50 @@ +import { createAction } from 'redux-actions'; +import { sortDirections } from 'Helpers/Props'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; + +export const section = 'tvShows'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + items: [], + sortKey: 'sortTitle', + sortDirection: sortDirections.ASCENDING, + pendingChanges: {} +}; + +export const FETCH_TV_SHOWS = 'tvShows/fetchTVShows'; +export const SET_TV_SHOW_VALUE = 'tvShows/setTVShowValue'; +export const SAVE_TV_SHOW = 'tvShows/saveTVShow'; +export const DELETE_TV_SHOW = 'tvShows/deleteTVShow'; + +export const fetchTVShows = createThunk(FETCH_TV_SHOWS); +export const saveTVShow = createThunk(SAVE_TV_SHOW); +export const deleteTVShow = createThunk(DELETE_TV_SHOW); + +export const setTVShowValue = createAction(SET_TV_SHOW_VALUE, (payload) => { + return { + section, + ...payload + }; +}); + +export const actionHandlers = handleThunks({ + [FETCH_TV_SHOWS]: createFetchHandler(section, '/tvshow'), + [SAVE_TV_SHOW]: createSaveProviderHandler(section, '/tvshow'), + [DELETE_TV_SHOW]: createRemoveItemHandler(section, '/tvshow') +}); + +export const reducers = createHandleActions({ + [SET_TV_SHOW_VALUE]: createSetSettingValueReducer(section) +}, defaultState, section); diff --git a/frontend/src/Store/Selectors/createAllTVShowsSelector.ts b/frontend/src/Store/Selectors/createAllTVShowsSelector.ts new file mode 100644 index 0000000000..eb0007c1b6 --- /dev/null +++ b/frontend/src/Store/Selectors/createAllTVShowsSelector.ts @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; + +function createAllTVShowsSelector() { + return createSelector( + (state: AppState) => state.tvShows, + (tvShows) => { + return tvShows.items; + } + ); +} + +export default createAllTVShowsSelector; diff --git a/frontend/src/TVShow/Details/TVShowDetails.css b/frontend/src/TVShow/Details/TVShowDetails.css new file mode 100644 index 0000000000..508f80d4f4 --- /dev/null +++ b/frontend/src/TVShow/Details/TVShowDetails.css @@ -0,0 +1,58 @@ +.container { + padding: 20px; +} + +.header { + display: flex; + align-items: flex-start; + margin-bottom: 20px; +} + +.title { + font-size: 32px; + font-weight: 300; + margin: 0 0 10px; +} + +.details { + margin-top: 20px; +} + +.detailRow { + display: flex; + margin-bottom: 10px; +} + +.label { + width: 150px; + font-weight: 500; + color: var(--labelColor); +} + +.value { + flex: 1; +} + +.overview { + margin-top: 20px; + padding: 15px; + background-color: var(--tableRowBackgroundColor); + border-radius: 4px; +} + +.monitoredIcon { + margin-left: 10px; +} + +.genres { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.genre { + background-color: var(--tableRowBackgroundColor); + padding: 2px 8px; + border-radius: 3px; + font-size: 12px; +} diff --git a/frontend/src/TVShow/Details/TVShowDetails.css.d.ts b/frontend/src/TVShow/Details/TVShowDetails.css.d.ts new file mode 100644 index 0000000000..94d2b414d1 --- /dev/null +++ b/frontend/src/TVShow/Details/TVShowDetails.css.d.ts @@ -0,0 +1,17 @@ +interface CSSModule { + readonly container: string; + readonly header: string; + readonly title: string; + readonly details: string; + readonly detailRow: string; + readonly label: string; + readonly value: string; + readonly overview: string; + readonly monitoredIcon: string; + readonly genres: string; + readonly genre: string; +} + +declare const styles: CSSModule; + +export default styles; diff --git a/frontend/src/TVShow/Details/TVShowDetails.tsx b/frontend/src/TVShow/Details/TVShowDetails.tsx new file mode 100644 index 0000000000..461a455f1e --- /dev/null +++ b/frontend/src/TVShow/Details/TVShowDetails.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Icon from 'Components/Icon'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './TVShowDetails.css'; + +interface TVShowDetailsProps { + readonly tvShowId: number; +} + +function TVShowDetails({ tvShowId }: Readonly) { + const tvShow = useSelector((state: AppState) => + state.tvShows.items.find((s) => s.id === tvShowId) + ); + + if (!tvShow) { + return null; + } + + const { + title, + year, + network, + status, + overview, + monitored, + seriesType, + isAnime, + genres, + runtime, + certification, + firstAired, + } = tvShow; + + return ( + + +
+
+

+ {title} ({year}) + +

+
+ +
+ {network && ( +
+ {translate('Network')}: + {network} +
+ )} + +
+ {translate('Status')}: + {status} +
+ +
+ {translate('SeriesType')}: + + {seriesType} {isAnime && '(Anime)'} + +
+ + {runtime && ( +
+ {translate('Runtime')}: + {runtime} min +
+ )} + + {certification && ( +
+ + {translate('Certification')}: + + {certification} +
+ )} + + {firstAired && ( +
+ {translate('FirstAired')}: + {firstAired} +
+ )} + + {genres && genres.length > 0 && ( +
+ {translate('Genres')}: +
+ {genres.map((genre) => ( + + {genre} + + ))} +
+
+ )} +
+ + {overview && ( +
+

{overview}

+
+ )} +
+
+
+ ); +} + +export default TVShowDetails; diff --git a/frontend/src/TVShow/Details/TVShowDetailsPage.tsx b/frontend/src/TVShow/Details/TVShowDetailsPage.tsx new file mode 100644 index 0000000000..ff1f107e50 --- /dev/null +++ b/frontend/src/TVShow/Details/TVShowDetailsPage.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory, useParams } from 'react-router'; +import NotFound from 'Components/NotFound'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import createAllTVShowsSelector from 'Store/Selectors/createAllTVShowsSelector'; +import translate from 'Utilities/String/translate'; +import TVShowDetails from './TVShowDetails'; + +function TVShowDetailsPage() { + const allTVShows = useSelector(createAllTVShowsSelector()); + const { id } = useParams<{ id: string }>(); + const history = useHistory(); + + const tvShowId = Number.parseInt(id); + const tvShowIndex = allTVShows.findIndex((tvShow) => tvShow.id === tvShowId); + + const previousIndex = usePrevious(tvShowIndex); + + useEffect(() => { + if ( + tvShowIndex === -1 && + previousIndex !== -1 && + previousIndex !== undefined + ) { + history.push(`${window.Radarr.urlBase}/tvshows`); + } + }, [tvShowIndex, previousIndex, history]); + + if (tvShowIndex === -1) { + return ; + } + + return ; +} + +export default TVShowDetailsPage; diff --git a/frontend/src/TVShow/Episode.ts b/frontend/src/TVShow/Episode.ts new file mode 100644 index 0000000000..d78bb91303 --- /dev/null +++ b/frontend/src/TVShow/Episode.ts @@ -0,0 +1,30 @@ +import ModelBase from 'App/ModelBase'; + +interface Episode extends ModelBase { + tvShowId?: number; + seasonId?: number; + seasonNumber: number; + episodeNumber: number; + absoluteEpisodeNumber?: number; + sceneSeasonNumber?: number; + sceneEpisodeNumber?: number; + sceneAbsoluteEpisodeNumber?: number; + title?: string; + overview?: string; + airDate?: string; + airDateUtc?: string; + runtime?: number; + isSpecial: boolean; + unverifiedSceneNumbering: boolean; + episodeFileId?: number; + monitored: boolean; + qualityProfileId: number; + path?: string; + rootFolderPath?: string; + added: string; + tags: number[]; + lastSearchTime?: string; + isSaving?: boolean; +} + +export default Episode; diff --git a/frontend/src/TVShow/Index/TVShowIndex.tsx b/frontend/src/TVShow/Index/TVShowIndex.tsx new file mode 100644 index 0000000000..35f52713a1 --- /dev/null +++ b/frontend/src/TVShow/Index/TVShowIndex.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +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 Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { icons, kinds } from 'Helpers/Props'; +import { fetchTVShows } from 'Store/Actions/tvShowActions'; +import translate from 'Utilities/String/translate'; +import TVShowIndexRow from './TVShowIndexRow'; + +const columns = [ + { + name: 'title', + label: () => translate('Title'), + isVisible: true, + }, + { + name: 'network', + label: () => translate('Network'), + isVisible: true, + }, + { + name: 'status', + label: () => translate('Status'), + isVisible: true, + }, + { + name: 'year', + label: () => translate('Year'), + isVisible: true, + }, + { + name: 'monitored', + label: () => translate('Monitored'), + isVisible: true, + }, +]; + +function TVShowIndex() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, error, items } = useSelector( + (state: AppState) => state.tvShows + ); + + useEffect(() => { + dispatch(fetchTVShows()); + }, [dispatch]); + + const onRefreshPress = useCallback(() => { + dispatch(fetchTVShows()); + }, [dispatch]); + + const hasNoTVShows = isPopulated && !items.length; + + return ( + + + + + + + + + {isFetching && !isPopulated ? : null} + + {!isFetching && !!error ? ( + {translate('UnableToLoadTVShows')} + ) : null} + + {isPopulated && !error && items.length > 0 ? ( + + + {items.map((tvShow) => ( + + ))} + +
+ ) : null} + + {hasNoTVShows ? ( +
+

{translate('NoTVShows')}

+

Add TV shows to track your series.

+
+ ) : null} +
+
+ ); +} + +export default TVShowIndex; diff --git a/frontend/src/TVShow/Index/TVShowIndexRow.tsx b/frontend/src/TVShow/Index/TVShowIndexRow.tsx new file mode 100644 index 0000000000..31be5dd295 --- /dev/null +++ b/frontend/src/TVShow/Index/TVShowIndexRow.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import { icons } from 'Helpers/Props'; +import TVShow from 'TVShow/TVShow'; + +function TVShowIndexRow(props: TVShow) { + const { id, title, network, status, year, monitored } = props; + + return ( + + + {title} + + {network || '-'} + {status} + {year} + + + + + ); +} + +export default TVShowIndexRow; diff --git a/frontend/src/TVShow/Season.ts b/frontend/src/TVShow/Season.ts new file mode 100644 index 0000000000..c3fe6c4fc0 --- /dev/null +++ b/frontend/src/TVShow/Season.ts @@ -0,0 +1,12 @@ +import ModelBase from 'App/ModelBase'; + +interface Season extends ModelBase { + tvShowId?: number; + seasonNumber: number; + title?: string; + overview?: string; + monitored: boolean; + isSaving?: boolean; +} + +export default Season; diff --git a/frontend/src/TVShow/TVShow.ts b/frontend/src/TVShow/TVShow.ts new file mode 100644 index 0000000000..09592a37f8 --- /dev/null +++ b/frontend/src/TVShow/TVShow.ts @@ -0,0 +1,39 @@ +import ModelBase from 'App/ModelBase'; + +export type TVShowStatus = 'continuing' | 'ended' | 'upcoming' | 'canceled'; +export type SeriesType = 'standard' | 'daily' | 'anime'; + +interface TVShow extends ModelBase { + tvdbId?: number; + tmdbId?: number; + imdbId?: string; + aniDbId?: number; + title: string; + sortTitle: string; + cleanTitle: string; + overview?: string; + network?: string; + status: TVShowStatus; + runtime?: number; + airTime?: string; + certification?: string; + firstAired?: string; + year: number; + genres: string[]; + originalLanguage?: string; + isAnime: boolean; + seriesType: SeriesType; + useSceneNumbering: boolean; + path?: string; + rootFolderPath?: string; + qualityProfileId: number; + seasonFolder: boolean; + monitored: boolean; + monitorNewItems: boolean; + added: string; + tags: number[]; + lastSearchTime?: string; + isSaving?: boolean; +} + +export default TVShow;