import classNames from "classnames"; import { ReactNode, useCallback, useEffect, useRef, 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 { ProgressRing } from "@/components/layout/ProgressRing"; 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 { Menu } from "@/components/player/internals/ContextMenu"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { PlayerMeta } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; import { useProgressStore } from "@/stores/progress"; import { hasAired } from "../utils/aired"; function CenteredText(props: { children: React.ReactNode }) { return (
{props.children}
); } function useSeasonData(mediaId: string, seasonId: string) { const [seasons, setSeason] = useState(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 { t } = useTranslation(); const meta = usePlayerStore((s) => s.meta); const [loadingState, seasons] = useSeasonData( meta?.tmdbId ?? "", selectedSeason, ); let content: ReactNode = null; if (seasons) { content = ( {seasons?.map((season) => { return ( setSeason(season.id)} > {season.title} ); })} ); } else if (loadingState.error) content = ( {t("player.menus.episodes.loadingError")} ); else if (loadingState.loading) content = ( {t("player.menus.episodes.loadingList")} ); return ( {meta?.title ?? t("player.menus.episodes.loadingTitle")} {content} ); } function EpisodesView({ id, selectedSeason, goBack, onChange, }: { id: string; selectedSeason: string; goBack?: () => void; onChange?: (meta: PlayerMeta) => void; }) { const { t } = useTranslation(); const router = useOverlayRouter(id); const { setPlayerMeta } = usePlayerMeta(); const meta = usePlayerStore((s) => s.meta); const [loadingState] = useSeasonData(meta?.tmdbId ?? "", selectedSeason); const progress = useProgressStore(); const playEpisode = useCallback( (episodeId: string) => { if (loadingState.value) { const newData = setPlayerMeta(loadingState.value.fullData, episodeId); if (newData) onChange?.(newData); } // prevent router clear here, otherwise its done double // player already switches route after meta change router.close(true); }, [setPlayerMeta, loadingState, router, onChange], ); if (!meta?.tmdbId) return null; let content: ReactNode = null; if (loadingState.error) content = ( {t("player.menus.episodes.loadingError")} ); else if (loadingState.loading) content = ( {t("player.menus.episodes.loadingList")} ); else if (loadingState.value) { const hasUnairedEpisodes = loadingState.value.season.episodes.some( (ep) => !hasAired(ep.air_date), ); content = ( {loadingState.value.season.episodes.length === 0 ? ( {t("player.menus.episodes.emptyState")} ) : null} {loadingState.value.season.episodes.map((ep) => { const episodeProgress = progress.items[meta?.tmdbId]?.episodes?.[ep.id]; let rightSide; if (episodeProgress) { const percentage = (episodeProgress.progress.watched / episodeProgress.progress.duration) * 100; rightSide = ( 90 ? 100 : percentage} /> ); } return ( playEpisode(ep.id)} active={ep.id === meta?.episode?.tmdbId} clickable={hasAired(ep.air_date)} rightSide={rightSide} >
{t("player.menus.episodes.episodeBadge", { episode: ep.number, })} {ep.title}
); })} {hasUnairedEpisodes ? (

{t("player.menus.episodes.unairedEpisodes")}

) : null}
); } return ( {loadingState?.value?.season.title || t("player.menus.episodes.loadingTitle")} } > {t("player.menus.episodes.seasons")} {content} ); } function EpisodesOverlay({ id, onChange, }: { id: string; onChange?: (meta: PlayerMeta) => void; }) { const router = useOverlayRouter(id); const meta = usePlayerStore((s) => s.meta); const [selectedSeason, setSelectedSeason] = useState(""); const lastActiveState = useRef(false); useEffect(() => { if (lastActiveState.current === router.isRouterActive) return; lastActiveState.current = router.isRouterActive; setSelectedSeason(meta?.season?.tmdbId ?? ""); }, [meta, selectedSeason, setSelectedSeason, router.isRouterActive]); const setSeason = useCallback( (seasonId: string) => { setSelectedSeason(seasonId); router.navigate("/episodes"); }, [router], ); return ( {selectedSeason.length > 0 ? ( router.navigate("/")} onChange={onChange} /> ) : null} ); } interface EpisodesProps { onChange?: (meta: PlayerMeta) => void; } export function EpisodesRouter(props: EpisodesProps) { return ; } 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 ( router.open("/episodes")} icon={Icons.EPISODES} > {t("player.menus.episodes.button")} ); }