diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts index 972326fcc9addc982263ce4ae32b50f8260e9bb2..8bcce03575f8da4d8b63bc75fe902b0af3810e92 100644 --- a/src/api/ApiClient.ts +++ b/src/api/ApiClient.ts @@ -30,7 +30,7 @@ export class ApiClient extends RequestClient { }); } - public async listEpisodes(contentId: string): Promise<Instalment> { + public async listEpisodes(contentId: string): Promise<Instalment[]> { return await this.request(`api/v1/content/${contentId}/episodes`, { method: "GET" }); diff --git a/src/api/models/Instalment.ts b/src/api/models/Instalment.ts index b3227098c410726e78878615e89baf53b55c17ef..efb6e85423db50e3a625f7fb7386290a5619b8b4 100644 --- a/src/api/models/Instalment.ts +++ b/src/api/models/Instalment.ts @@ -6,3 +6,37 @@ export interface Instalment { airDate: string | null, content: Content } + +export function sortInstalments(instalments: Instalment[]): Instalment[] { + return instalments.sort((a, b) => { + if (a.season !== b.season) { + if (a.season === null) return 1; + if (b.season === null) return -1; + return compareNumericStrings(a.season, b.season); + } else if (a.episode !== b.episode) { + if (a.episode === null) return 1; + if (b.episode === null) return -1; + return compareNumericStrings(a.episode, b.episode); + } else { + if (a.airDate === null) return 1; + if (b.airDate === null) return -1; + return compareDateStrings(a.airDate, b.airDate); + } + }) +} + +function compareDateStrings(a: string, b: string) { + return new Date(a).getTime() - new Date(b).getTime(); +} + +function compareNumericStrings(a: string, b: string) { + if (isNumeric(a) && isNumeric(b)) { + return parseFloat(a) - parseFloat(b); + } else { + return a.localeCompare(b); + } +} + +function isNumeric(data: string): boolean { + return !isNaN(data as any as number) && !isNaN(parseFloat(data)) +} diff --git a/src/routes/main/MainPage.tsx b/src/routes/main/MainPage.tsx index ddd6cb56a1c6e047dfa88f6bad22c554031ce34a..18cc5a1c820832f51b28463ad05e808b304729da 100644 --- a/src/routes/main/MainPage.tsx +++ b/src/routes/main/MainPage.tsx @@ -61,7 +61,7 @@ export function MainPage(props: Props) { const episodeTitle = getLocalizedName(episode.content, locale); const episodeDescription = getLocalizedDescription(episode.content, locale); return ( - <li> + <li key={episode.content.ids.uuid}> <p> <strong>S{episode.season}E{episode.episode}</strong> – {episode.airDate} </p> diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx index 54aa50c1bbf3fcf91ebadadf87789dbbaafece83..a4cfea04a7c6f13ad29fd225fb7b14548141ae72 100644 --- a/src/routes/player/Player.tsx +++ b/src/routes/player/Player.tsx @@ -1,4 +1,4 @@ -import {useState} from "react"; +import {Fragment, useEffect, useState} from "react"; import {createUseStyles} from "react-jss"; import {Link} from "react-router-dom"; import {ContentMeta} from "../../api/models/dto/ContentMeta"; @@ -17,6 +17,8 @@ import {Subtitle} from "../../api/models/Subtitle"; import {MousePosition} from "../../util/mouse/MousePosition"; import {VideoProvider} from "./video/VideoContext"; import {SubtitleRenderer} from "./subtitles/SubtitleRenderer"; +import {useApiClient} from "../../api/ApiClientContext"; +import {Instalment, sortInstalments} from "../../api/models/Instalment"; interface Props { meta: ContentMeta, @@ -26,10 +28,20 @@ interface Props { export function Player( {meta, media}: Props ) { + const apiClient = useApiClient(); const {content, instalment} = meta; const classes = useStyles(); const locale = useLocale(); + const [relatedInstalments, setRelatedInstalments] = useState<Instalment[]>([]); + useEffect(() => { + setRelatedInstalments([]); + if (instalment?.content.ids.uuid !== undefined) { + apiClient.listEpisodes(instalment.content.ids.uuid) + .then(result => setRelatedInstalments(sortInstalments(result))); + } + }, [apiClient, instalment]); + const name = getLocalizedName(content, locale); const description = getLocalizedDescription(content, locale); const rating = getLocalizedRating(content, locale); @@ -58,7 +70,34 @@ export function Player( <p>{rating?.certification}</p> <strong>{description?.tagline}</strong> <p>{description?.overview}</p> - <p>{instalment?.content && getLocalizedName(instalment?.content, locale)?.name}</p> + {instalment?.content && ( + <Fragment> + <h3>{getLocalizedName(instalment.content, locale)?.name}</h3> + <ol> + {relatedInstalments.map(episode => { + const episodeTitle = getLocalizedName(episode.content, locale); + const episodeDescription = getLocalizedDescription(episode.content, locale); + return ( + <li key={episode.content.ids.uuid}> + <p> + <strong>S{episode.season}E{episode.episode}</strong> – {episode.airDate} + </p> + <p><strong>{episodeTitle?.name}</strong></p> + <p><strong>{episodeDescription?.tagline}</strong></p> + <p>{episodeDescription?.overview}</p> + {episode.content.ids.uuid !== meta.content.ids.uuid ? ( + <p> + <Link to={"/player/" + episode.content.ids.uuid}>Play</Link> + </p> + ) : ( + <p><u>Playing</u></p> + )} + </li> + ) + })} + </ol> + </Fragment> + )} <div className={classes.player}> <div className={classes.playerCanvas}> <VideoElement