mirror of
https://github.com/Radarr/Radarr
synced 2026-01-25 08:53:02 +01:00
feat(tv): add TV show frontend components
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
b9e086cca6
commit
89815c1d7a
21 changed files with 678 additions and 0 deletions
|
|
@ -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() {
|
|||
|
||||
<Route path="/bookseries/:id" component={BookSeriesDetailsPage} />
|
||||
|
||||
{/*
|
||||
TV Shows
|
||||
*/}
|
||||
|
||||
<Route exact={true} path="/tvshows" component={TVShowIndex} />
|
||||
|
||||
<Route path="/tvshow/:id" component={TVShowDetailsPage} />
|
||||
|
||||
{/*
|
||||
Calendar
|
||||
*/}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
12
frontend/src/App/State/EpisodesAppState.ts
Normal file
12
frontend/src/App/State/EpisodesAppState.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Episode from 'TVShow/Episode';
|
||||
|
||||
interface EpisodesAppState
|
||||
extends AppSectionState<Episode>, AppSectionDeleteState, AppSectionSaveState {
|
||||
pendingChanges: Partial<Episode>;
|
||||
}
|
||||
|
||||
export default EpisodesAppState;
|
||||
12
frontend/src/App/State/SeasonsAppState.ts
Normal file
12
frontend/src/App/State/SeasonsAppState.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Season from 'TVShow/Season';
|
||||
|
||||
interface SeasonsAppState
|
||||
extends AppSectionState<Season>, AppSectionDeleteState, AppSectionSaveState {
|
||||
pendingChanges: Partial<Season>;
|
||||
}
|
||||
|
||||
export default SeasonsAppState;
|
||||
12
frontend/src/App/State/TVShowsAppState.ts
Normal file
12
frontend/src/App/State/TVShowsAppState.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import TVShow from 'TVShow/TVShow';
|
||||
|
||||
interface TVShowsAppState
|
||||
extends AppSectionState<TVShow>, AppSectionDeleteState, AppSectionSaveState {
|
||||
pendingChanges: Partial<TVShow>;
|
||||
}
|
||||
|
||||
export default TVShowsAppState;
|
||||
|
|
@ -119,6 +119,12 @@ const LINKS: SidebarItem[] = [
|
|||
to: '/bookseries',
|
||||
},
|
||||
|
||||
{
|
||||
iconName: icons.TV,
|
||||
title: () => translate('TVShows'),
|
||||
to: '/tvshows',
|
||||
},
|
||||
|
||||
{
|
||||
iconName: icons.CALENDAR,
|
||||
title: () => translate('Calendar'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
50
frontend/src/Store/Actions/episodeActions.js
Normal file
50
frontend/src/Store/Actions/episodeActions.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
];
|
||||
|
|
|
|||
50
frontend/src/Store/Actions/seasonActions.js
Normal file
50
frontend/src/Store/Actions/seasonActions.js
Normal file
|
|
@ -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);
|
||||
50
frontend/src/Store/Actions/tvShowActions.js
Normal file
50
frontend/src/Store/Actions/tvShowActions.js
Normal file
|
|
@ -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);
|
||||
13
frontend/src/Store/Selectors/createAllTVShowsSelector.ts
Normal file
13
frontend/src/Store/Selectors/createAllTVShowsSelector.ts
Normal file
|
|
@ -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;
|
||||
58
frontend/src/TVShow/Details/TVShowDetails.css
Normal file
58
frontend/src/TVShow/Details/TVShowDetails.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
17
frontend/src/TVShow/Details/TVShowDetails.css.d.ts
vendored
Normal file
17
frontend/src/TVShow/Details/TVShowDetails.css.d.ts
vendored
Normal file
|
|
@ -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;
|
||||
123
frontend/src/TVShow/Details/TVShowDetails.tsx
Normal file
123
frontend/src/TVShow/Details/TVShowDetails.tsx
Normal file
|
|
@ -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<TVShowDetailsProps>) {
|
||||
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 (
|
||||
<PageContent title={title}>
|
||||
<PageContentBody>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>
|
||||
{title} ({year})
|
||||
<Icon
|
||||
className={styles.monitoredIcon}
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
title={monitored ? 'Monitored' : 'Unmonitored'}
|
||||
size={24}
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
{network && (
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>{translate('Network')}:</span>
|
||||
<span className={styles.value}>{network}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>{translate('Status')}:</span>
|
||||
<span className={styles.value}>{status}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>{translate('SeriesType')}:</span>
|
||||
<span className={styles.value}>
|
||||
{seriesType} {isAnime && '(Anime)'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{runtime && (
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>{translate('Runtime')}:</span>
|
||||
<span className={styles.value}>{runtime} min</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{certification && (
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>
|
||||
{translate('Certification')}:
|
||||
</span>
|
||||
<span className={styles.value}>{certification}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{firstAired && (
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>{translate('FirstAired')}:</span>
|
||||
<span className={styles.value}>{firstAired}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{genres && genres.length > 0 && (
|
||||
<div className={styles.detailRow}>
|
||||
<span className={styles.label}>{translate('Genres')}:</span>
|
||||
<div className={styles.genres}>
|
||||
{genres.map((genre) => (
|
||||
<span key={genre} className={styles.genre}>
|
||||
{genre}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{overview && (
|
||||
<div className={styles.overview}>
|
||||
<p>{overview}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default TVShowDetails;
|
||||
37
frontend/src/TVShow/Details/TVShowDetailsPage.tsx
Normal file
37
frontend/src/TVShow/Details/TVShowDetailsPage.tsx
Normal file
|
|
@ -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 <NotFound message={translate('TVShowCannotBeFound')} />;
|
||||
}
|
||||
|
||||
return <TVShowDetails tvShowId={allTVShows[tvShowIndex].id} />;
|
||||
}
|
||||
|
||||
export default TVShowDetailsPage;
|
||||
30
frontend/src/TVShow/Episode.ts
Normal file
30
frontend/src/TVShow/Episode.ts
Normal file
|
|
@ -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;
|
||||
103
frontend/src/TVShow/Index/TVShowIndex.tsx
Normal file
103
frontend/src/TVShow/Index/TVShowIndex.tsx
Normal file
|
|
@ -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 (
|
||||
<PageContent title={translate('TVShows')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshAll')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('UnableToLoadTVShows')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && items.length > 0 ? (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((tvShow) => (
|
||||
<TVShowIndexRow key={tvShow.id} {...tvShow} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{hasNoTVShows ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<p>{translate('NoTVShows')}</p>
|
||||
<p>Add TV shows to track your series.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default TVShowIndex;
|
||||
30
frontend/src/TVShow/Index/TVShowIndexRow.tsx
Normal file
30
frontend/src/TVShow/Index/TVShowIndexRow.tsx
Normal file
|
|
@ -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 (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
<Link to={`/tvshow/${id}`}>{title}</Link>
|
||||
</TableRowCell>
|
||||
<TableRowCell>{network || '-'}</TableRowCell>
|
||||
<TableRowCell>{status}</TableRowCell>
|
||||
<TableRowCell>{year}</TableRowCell>
|
||||
<TableRowCell>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
title={monitored ? 'Monitored' : 'Unmonitored'}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default TVShowIndexRow;
|
||||
12
frontend/src/TVShow/Season.ts
Normal file
12
frontend/src/TVShow/Season.ts
Normal file
|
|
@ -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;
|
||||
39
frontend/src/TVShow/TVShow.ts
Normal file
39
frontend/src/TVShow/TVShow.ts
Normal file
|
|
@ -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;
|
||||
Loading…
Reference in a new issue