23 changed files with 391 additions and 110 deletions
@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
|
||||
import { ReactNode, useCallback, useEffect, useState } from "react"; |
||||
import { useTranslation } from "react-i18next"; |
||||
import { useAsync } from "react-use"; |
||||
|
||||
import { getMetaFromId } from "@/backend/metadata/getmeta"; |
||||
import { MWMediaType, MWSeasonMeta } from "@/backend/metadata/types/mw"; |
||||
import { Icons } from "@/components/Icon"; |
||||
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor"; |
||||
import { Overlay } from "@/components/overlays/OverlayDisplay"; |
||||
import { OverlayPage } from "@/components/overlays/OverlayPage"; |
||||
import { OverlayRouter } from "@/components/overlays/OverlayRouter"; |
||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; |
||||
import { VideoPlayerButton } from "@/components/player/internals/Button"; |
||||
import { Context } from "@/components/player/internals/ContextUtils"; |
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; |
||||
import { usePlayerStore } from "@/stores/player/store"; |
||||
|
||||
function CenteredText(props: { children: React.ReactNode }) { |
||||
return ( |
||||
<div className="h-full w-full flex justify-center items-center p-8 text-center"> |
||||
{props.children} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function useSeasonData(mediaId: string, seasonId: string) { |
||||
const [seasons, setSeason] = useState<MWSeasonMeta[] | null>(null); |
||||
|
||||
const state = useAsync(async () => { |
||||
const data = await getMetaFromId(MWMediaType.SERIES, mediaId, seasonId); |
||||
if (data?.meta.type !== MWMediaType.SERIES) return null; |
||||
setSeason(data.meta.seasons); |
||||
return { |
||||
season: data.meta.seasonData, |
||||
fullData: data, |
||||
}; |
||||
}, [mediaId, seasonId]); |
||||
|
||||
return [state, seasons] as const; |
||||
} |
||||
|
||||
function SeasonsView({ |
||||
selectedSeason, |
||||
setSeason, |
||||
}: { |
||||
selectedSeason: string; |
||||
setSeason: (id: string) => void; |
||||
}) { |
||||
const meta = usePlayerStore((s) => s.meta); |
||||
const [loadingState, seasons] = useSeasonData( |
||||
meta?.tmdbId ?? "", |
||||
selectedSeason |
||||
); |
||||
|
||||
let content: ReactNode = null; |
||||
if (seasons) { |
||||
content = ( |
||||
<Context.Section className="pb-6"> |
||||
{seasons?.map((season) => { |
||||
return ( |
||||
<Context.Link key={season.id} onClick={() => setSeason(season.id)}> |
||||
<Context.LinkTitle>{season.title}</Context.LinkTitle> |
||||
<Context.LinkChevron /> |
||||
</Context.Link> |
||||
); |
||||
})} |
||||
</Context.Section> |
||||
); |
||||
} else if (loadingState.error) |
||||
content = <CenteredText>Error loading season</CenteredText>; |
||||
else if (loadingState.loading) |
||||
content = <CenteredText>Loading...</CenteredText>; |
||||
|
||||
return ( |
||||
<Context.CardWithScrollable> |
||||
<Context.Title>{meta?.title}</Context.Title> |
||||
{content} |
||||
</Context.CardWithScrollable> |
||||
); |
||||
} |
||||
|
||||
function EpisodesView({ |
||||
id, |
||||
selectedSeason, |
||||
goBack, |
||||
}: { |
||||
id: string; |
||||
selectedSeason: string; |
||||
goBack?: () => void; |
||||
}) { |
||||
const { t } = useTranslation(); |
||||
const router = useOverlayRouter(id); |
||||
const { setPlayerMeta } = usePlayerMeta(); |
||||
const meta = usePlayerStore((s) => s.meta); |
||||
const [loadingState] = useSeasonData(meta?.tmdbId ?? "", selectedSeason); |
||||
|
||||
const playEpisode = useCallback( |
||||
(episodeId: string) => { |
||||
if (loadingState.value) |
||||
setPlayerMeta(loadingState.value.fullData, episodeId); |
||||
router.close(); |
||||
}, |
||||
[setPlayerMeta, loadingState, router] |
||||
); |
||||
|
||||
let content: ReactNode = null; |
||||
if (loadingState.error) |
||||
content = <CenteredText>Error loading season</CenteredText>; |
||||
else if (loadingState.loading) |
||||
content = <CenteredText>Loading...</CenteredText>; |
||||
else if (loadingState.value) { |
||||
content = ( |
||||
<Context.Section className="pb-6"> |
||||
{loadingState.value.season.episodes.map((ep) => { |
||||
return ( |
||||
<Context.Link |
||||
key={ep.id} |
||||
onClick={() => playEpisode(ep.id)} |
||||
active={ep.id === meta?.episode?.tmdbId} |
||||
> |
||||
<Context.LinkTitle> |
||||
<div className="text-left flex items-center space-x-3"> |
||||
<span className="p-0.5 px-2 rounded inline bg-video-context-border bg-opacity-10"> |
||||
E{ep.number} |
||||
</span> |
||||
<span className="line-clamp-1 break-all">{ep.title}</span> |
||||
</div> |
||||
</Context.LinkTitle> |
||||
<Context.LinkChevron /> |
||||
</Context.Link> |
||||
); |
||||
})} |
||||
</Context.Section> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<Context.CardWithScrollable> |
||||
<Context.BackLink onClick={goBack}> |
||||
{loadingState?.value?.season.title || t("videoPlayer.loading")} |
||||
</Context.BackLink> |
||||
{content} |
||||
</Context.CardWithScrollable> |
||||
); |
||||
} |
||||
|
||||
function EpisodesOverlay({ id }: { id: string }) { |
||||
const router = useOverlayRouter(id); |
||||
const meta = usePlayerStore((s) => s.meta); |
||||
const [selectedSeason, setSelectedSeason] = useState( |
||||
meta?.season?.tmdbId ?? "" |
||||
); |
||||
|
||||
const setSeason = useCallback( |
||||
(seasonId: string) => { |
||||
setSelectedSeason(seasonId); |
||||
router.navigate("/episodes"); |
||||
}, |
||||
[router] |
||||
); |
||||
|
||||
return ( |
||||
<Overlay id={id}> |
||||
<OverlayRouter id={id}> |
||||
<OverlayPage id={id} path="/" width={343} height={431}> |
||||
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} /> |
||||
</OverlayPage> |
||||
<OverlayPage id={id} path="/episodes" width={343} height={431}> |
||||
<EpisodesView |
||||
selectedSeason={selectedSeason} |
||||
id={id} |
||||
goBack={() => router.navigate("/")} |
||||
/> |
||||
</OverlayPage> |
||||
</OverlayRouter> |
||||
</Overlay> |
||||
); |
||||
} |
||||
|
||||
export function Episodes() { |
||||
const { t } = useTranslation(); |
||||
const router = useOverlayRouter("episodes"); |
||||
const setHasOpenOverlay = usePlayerStore((s) => s.setHasOpenOverlay); |
||||
const type = usePlayerStore((s) => s.meta?.type); |
||||
|
||||
useEffect(() => { |
||||
setHasOpenOverlay(router.isRouterActive); |
||||
}, [setHasOpenOverlay, router.isRouterActive]); |
||||
|
||||
if (type !== "show") return null; |
||||
|
||||
return ( |
||||
<OverlayAnchor id={router.id}> |
||||
<VideoPlayerButton |
||||
onClick={() => router.open("/episodes")} |
||||
icon={Icons.EPISODES} |
||||
> |
||||
{t("videoPlayer.buttons.episodes")} |
||||
</VideoPlayerButton> |
||||
<EpisodesOverlay id={router.id} /> |
||||
</OverlayAnchor> |
||||
); |
||||
} |
Loading…
Reference in new issue